diff --git a/.gitmodules b/.gitmodules index 5bdc246..db73ea3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,8 +15,8 @@ url = https://github.com/Pier-Two/leanSpec.git [submodule "tools/lean-quickstart"] path = tools/lean-quickstart - url = https://github.com/Pier-Two/lean-quickstart.git - branch = devnet3 + url = https://github.com/blockblaz/lean-quickstart.git + branch = main [submodule "external/c-hash-sig"] path = external/c-hash-sig url = https://github.com/Pier-Two/c-hash-sig.git diff --git a/CMakeLists.txt b/CMakeLists.txt index b3786f8..4d17a48 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ add_library(lantern STATIC src/consensus/slot_clock.c src/consensus/signature.c src/consensus/state.c + src/consensus/store.c src/consensus/ssz.c src/core/client.c src/core/client_debug.c @@ -246,10 +247,12 @@ if(LANTERN_BUILD_TESTS) add_test(NAME lantern_consensus_runtime COMMAND lantern_consensus_runtime_test) add_executable(lantern_state_test tests/unit/test_state.c) + target_sources(lantern_state_test PRIVATE tests/support/state_store_adapter.c) target_link_libraries(lantern_state_test PRIVATE lantern) add_test(NAME lantern_state COMMAND lantern_state_test) add_executable(lantern_off_head_replay_test tests/unit/test_off_head_replay.c) + target_sources(lantern_off_head_replay_test PRIVATE tests/support/state_store_adapter.c) target_link_libraries(lantern_off_head_replay_test PRIVATE lantern) add_test(NAME lantern_off_head_replay COMMAND lantern_off_head_replay_test) set_tests_properties(lantern_off_head_replay PROPERTIES TIMEOUT 120) @@ -265,6 +268,7 @@ if(LANTERN_BUILD_TESTS) add_executable(lantern_networking_messages_test tests/unit/test_networking_messages.c + tests/support/state_store_adapter.c tests/support/fixture_loader.c ) target_link_libraries(lantern_networking_messages_test PRIVATE lantern) @@ -290,6 +294,7 @@ if(LANTERN_BUILD_TESTS) add_test(NAME lantern_fork_choice COMMAND lantern_fork_choice_test) add_executable(lantern_storage_test tests/unit/test_storage.c) + target_sources(lantern_storage_test PRIVATE tests/support/state_store_adapter.c) target_link_libraries(lantern_storage_test PRIVATE lantern) add_test(NAME lantern_storage COMMAND lantern_storage_test) @@ -308,6 +313,7 @@ if(LANTERN_BUILD_TESTS) add_executable(lantern_consensus_vectors_test tests/integration/test_consensus_vectors.c tests/integration/consensus_fixture_runner.c + tests/support/state_store_adapter.c tests/support/fixture_loader.c ) target_link_libraries(lantern_consensus_vectors_test PRIVATE lantern) @@ -330,6 +336,7 @@ if(LANTERN_BUILD_TESTS) add_executable(lantern_verify_signatures_vectors_test tests/integration/test_verify_signatures_vectors.c + tests/support/state_store_adapter.c tests/support/fixture_loader.c ) target_link_libraries(lantern_verify_signatures_vectors_test PRIVATE lantern) @@ -350,6 +357,7 @@ if(LANTERN_BUILD_TESTS) add_executable(lantern_genesis_vectors_test tests/integration/test_genesis_vectors.c tests/integration/consensus_fixture_runner.c + tests/support/state_store_adapter.c tests/support/fixture_loader.c ) target_link_libraries(lantern_genesis_vectors_test PRIVATE lantern) @@ -412,5 +420,6 @@ if(LANTERN_BUILD_TESTS) ) set_property(TEST ${_lantern_ctest_targets} PROPERTY TIMEOUT 30) set_tests_properties(lantern_client_vote PROPERTIES TIMEOUT 300) + set_tests_properties(lantern_state PROPERTIES TIMEOUT 120) endif() diff --git a/include/lantern/consensus/fork_choice.h b/include/lantern/consensus/fork_choice.h index 7c962cd..31cc3be 100644 --- a/include/lantern/consensus/fork_choice.h +++ b/include/lantern/consensus/fork_choice.h @@ -6,6 +6,7 @@ #include #include "lantern/consensus/containers.h" +#include "lantern/consensus/state.h" #ifdef __cplusplus extern "C" { @@ -14,8 +15,12 @@ extern "C" { struct lantern_fork_choice_vote_entry { bool has_checkpoint; LanternCheckpoint checkpoint; + uint64_t slot; }; +struct lantern_aggregated_payload_pool; +struct lantern_attestation_data_by_root; + struct lantern_fork_choice_block_entry { LanternRoot root; LanternRoot parent_root; @@ -29,6 +34,11 @@ struct lantern_fork_choice_block_entry { LanternCheckpoint latest_finalized; }; +struct lantern_fork_choice_state_entry { + bool has_state; + LanternState state; +}; + struct lantern_fork_choice_root_index_entry { LanternRoot root; size_t value; @@ -55,6 +65,9 @@ typedef struct lantern_fork_choice { size_t block_len; size_t block_cap; + struct lantern_fork_choice_state_entry *states; + size_t state_cap; + struct lantern_fork_choice_root_index_entry *index_entries; size_t index_cap; size_t index_len; @@ -62,6 +75,17 @@ typedef struct lantern_fork_choice { struct lantern_fork_choice_vote_entry *known_votes; struct lantern_fork_choice_vote_entry *new_votes; size_t validator_count; + + /* + * Attached views into store-owned attestation material. + * + * Aggregated gossip updates the shared store first. Fork-choice consumers + * read these pointers to observe the same proof pools and attestation-data + * map without maintaining a duplicate cache. + */ + const struct lantern_aggregated_payload_pool *new_aggregated_payloads; + const struct lantern_aggregated_payload_pool *known_aggregated_payloads; + const struct lantern_attestation_data_by_root *attestation_data_by_root; } LanternForkChoice; void lantern_fork_choice_init(LanternForkChoice *store); @@ -75,6 +99,13 @@ int lantern_fork_choice_set_anchor( const LanternCheckpoint *latest_justified, const LanternCheckpoint *latest_finalized, const LanternRoot *block_root_hint); +int lantern_fork_choice_set_anchor_with_state( + LanternForkChoice *store, + const LanternBlock *anchor_block, + const LanternCheckpoint *latest_justified, + const LanternCheckpoint *latest_finalized, + const LanternRoot *block_root_hint, + const LanternState *anchor_state); int lantern_fork_choice_add_block( LanternForkChoice *store, @@ -83,6 +114,14 @@ int lantern_fork_choice_add_block( const LanternCheckpoint *post_justified, const LanternCheckpoint *post_finalized, const LanternRoot *block_root_hint); +int lantern_fork_choice_add_block_with_state( + LanternForkChoice *store, + const LanternBlock *block, + const LanternSignedVote *proposer_attestation, + const LanternCheckpoint *post_justified, + const LanternCheckpoint *post_finalized, + const LanternRoot *block_root_hint, + const LanternState *post_state); int lantern_fork_choice_add_vote( LanternForkChoice *store, @@ -128,6 +167,13 @@ int lantern_fork_choice_set_block_validator_count( LanternForkChoice *store, const LanternRoot *root, uint64_t validator_count); +int lantern_fork_choice_set_block_state( + LanternForkChoice *store, + const LanternRoot *root, + const LanternState *state); +const LanternState *lantern_fork_choice_block_state( + const LanternForkChoice *store, + const LanternRoot *root); const LanternCheckpoint *lantern_fork_choice_latest_justified(const LanternForkChoice *store); const LanternCheckpoint *lantern_fork_choice_latest_finalized(const LanternForkChoice *store); const LanternRoot *lantern_fork_choice_safe_target(const LanternForkChoice *store); diff --git a/include/lantern/consensus/state.h b/include/lantern/consensus/state.h index 4042a7a..8f7d327 100644 --- a/include/lantern/consensus/state.h +++ b/include/lantern/consensus/state.h @@ -7,8 +7,7 @@ #include "lantern/consensus/containers.h" -struct lantern_vote_record; -struct lantern_fork_choice; +typedef struct lantern_store LanternStore; struct lantern_root_list { LanternRoot *items; @@ -23,18 +22,12 @@ typedef struct { LanternCheckpoint latest_justified; LanternCheckpoint latest_finalized; struct lantern_root_list historical_block_hashes; - uint64_t historical_roots_offset; struct lantern_bitlist justified_slots; - uint64_t justified_slots_offset; struct lantern_root_list justification_roots; struct lantern_bitlist justification_validators; - struct lantern_vote_record *validator_votes; - size_t validator_votes_len; LanternValidator *validators; size_t validator_count; size_t validator_capacity; - struct lantern_fork_choice *fork_choice; - LanternRoot validator_registry_root; } LanternState; void lantern_root_list_init(struct lantern_root_list *list); @@ -44,44 +37,35 @@ int lantern_root_list_resize(struct lantern_root_list *list, size_t new_length); void lantern_state_init(LanternState *state); void lantern_state_reset(LanternState *state); int lantern_state_clone(const LanternState *source, LanternState *dest); -void lantern_state_attach_fork_choice(LanternState *state, struct lantern_fork_choice *fork_choice); int lantern_state_generate_genesis(LanternState *state, uint64_t genesis_time, uint64_t num_validators); int lantern_state_process_slot(LanternState *state); int lantern_state_process_slots(LanternState *state, uint64_t target_slot); int lantern_state_process_block_header(LanternState *state, const LanternBlock *block); int lantern_state_process_attestations( LanternState *state, + LanternStore *store, const LanternAttestations *attestations, const LanternSignatureList *signatures); int lantern_state_process_block( LanternState *state, + LanternStore *store, const LanternBlock *block, const LanternBlockSignatures *signatures, const LanternSignedVote *proposer_attestation); 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); int lantern_state_mark_justified_slot(LanternState *state, uint64_t slot); -int lantern_state_transition(LanternState *state, const LanternSignedBlock *signed_block); -int lantern_state_prepare_validator_votes(LanternState *state, uint64_t validator_count); -size_t lantern_state_validator_capacity(const LanternState *state); -bool lantern_state_validator_has_vote(const LanternState *state, size_t index); -int lantern_state_get_signed_validator_vote( - const LanternState *state, - size_t index, - LanternSignedVote *out_vote); -int lantern_state_get_validator_vote(const LanternState *state, size_t index, LanternVote *out_vote); -int lantern_state_set_signed_validator_vote( - LanternState *state, - size_t index, - const LanternSignedVote *vote); -int lantern_state_set_validator_vote(LanternState *state, size_t index, const LanternVote *vote); -void lantern_state_clear_validator_vote(LanternState *state, size_t index); +int lantern_state_transition(LanternState *state, LanternStore *store, const LanternSignedBlock *signed_block); int lantern_state_set_validator_pubkeys(LanternState *state, const uint8_t *pubkeys, size_t count); size_t lantern_state_validator_count(const LanternState *state); const uint8_t *lantern_state_validator_pubkey(const LanternState *state, size_t index); -int lantern_state_select_block_parent(LanternState *state, LanternRoot *out_parent_root); +int lantern_state_select_block_parent( + LanternState *state, + const LanternStore *store, + LanternRoot *out_parent_root); int lantern_state_collect_attestations_for_block( const LanternState *state, + const LanternStore *store, uint64_t block_slot, uint64_t proposer_index, const LanternRoot *parent_root, @@ -90,11 +74,13 @@ int lantern_state_collect_attestations_for_block( LanternSignatureList *out_signatures); int lantern_state_compute_vote_checkpoints( const LanternState *state, + const LanternStore *store, LanternCheckpoint *out_head, LanternCheckpoint *out_target, LanternCheckpoint *out_source); int lantern_state_preview_post_state_root( const LanternState *state, + const LanternStore *store, const LanternSignedBlock *block, LanternRoot *out_state_root); diff --git a/include/lantern/consensus/store.h b/include/lantern/consensus/store.h new file mode 100644 index 0000000..a4c8370 --- /dev/null +++ b/include/lantern/consensus/store.h @@ -0,0 +1,138 @@ +#ifndef LANTERN_CONSENSUS_STORE_H +#define LANTERN_CONSENSUS_STORE_H + +#include +#include +#include + +#include "lantern/consensus/containers.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct lantern_fork_choice; +struct lantern_fork_choice_vote_entry; + +struct lantern_vote_record { + LanternVote vote; + LanternSignature signature; + bool has_vote; + bool has_signature; +}; + +typedef struct { + LanternValidatorIndex validator_index; + LanternRoot data_root; +} LanternSignatureKey; + +struct lantern_gossip_signature_entry { + LanternSignatureKey key; + LanternSignature signature; + uint64_t target_slot; +}; + +struct lantern_gossip_signature_map { + struct lantern_gossip_signature_entry *entries; + size_t length; + size_t capacity; +}; + +struct lantern_aggregated_payload_entry { + LanternRoot data_root; + LanternAggregatedSignatureProof proof; + uint64_t target_slot; +}; + +struct lantern_aggregated_payload_pool { + struct lantern_aggregated_payload_entry *entries; + size_t length; + size_t capacity; +}; + +struct lantern_attestation_data_by_root_entry { + LanternRoot data_root; + LanternAttestationData data; + uint64_t target_slot; +}; + +struct lantern_attestation_data_by_root { + struct lantern_attestation_data_by_root_entry *entries; + size_t length; + size_t capacity; +}; + +typedef struct lantern_store { + struct lantern_vote_record *validator_votes; + size_t validator_votes_len; + + struct lantern_fork_choice_vote_entry *known_votes; + struct lantern_fork_choice_vote_entry *new_votes; + size_t fork_choice_vote_count; + + struct lantern_gossip_signature_map gossip_signatures; + struct lantern_aggregated_payload_pool new_aggregated_payloads; + struct lantern_aggregated_payload_pool known_aggregated_payloads; + struct lantern_attestation_data_by_root attestation_data_by_root; + + struct lantern_fork_choice *fork_choice; +} LanternStore; + +void lantern_store_init(LanternStore *store); +void lantern_store_reset(LanternStore *store); +void lantern_store_attach_fork_choice(LanternStore *store, struct lantern_fork_choice *fork_choice); + +int lantern_store_prepare_validator_votes(LanternStore *store, uint64_t validator_count); +int lantern_store_clone_validator_votes(const LanternStore *source, LanternStore *dest); +size_t lantern_store_validator_capacity(const LanternStore *store); +bool lantern_store_validator_has_vote(const LanternStore *store, size_t index); +int lantern_store_get_signed_validator_vote( + const LanternStore *store, + size_t index, + LanternSignedVote *out_vote); +int lantern_store_get_validator_vote(const LanternStore *store, size_t index, LanternVote *out_vote); +int lantern_store_set_signed_validator_vote( + LanternStore *store, + size_t index, + const LanternSignedVote *vote); +int lantern_store_set_validator_vote(LanternStore *store, size_t index, const LanternVote *vote); +void lantern_store_clear_validator_vote(LanternStore *store, size_t index); + +int lantern_store_prepare_fork_choice_votes(LanternStore *store, uint64_t validator_count); + +int lantern_store_set_gossip_signature( + LanternStore *store, + const LanternSignatureKey *key, + const LanternAttestationData *data, + const LanternSignature *signature, + uint64_t target_slot); +int lantern_store_get_gossip_signature( + const LanternStore *store, + const LanternSignatureKey *key, + LanternSignature *out_signature); +int lantern_store_add_new_aggregated_payload( + LanternStore *store, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot); +int lantern_store_add_known_aggregated_payload( + LanternStore *store, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot); +size_t lantern_store_promote_new_aggregated_payloads(LanternStore *store); +size_t lantern_store_prune_finalized_attestation_material( + LanternStore *store, + uint64_t finalized_slot); +int lantern_store_get_attestation_data( + const LanternStore *store, + const LanternRoot *data_root, + LanternAttestationData *out_data); + +#ifdef __cplusplus +} +#endif + +#endif /* LANTERN_CONSENSUS_STORE_H */ diff --git a/include/lantern/core/client.h b/include/lantern/core/client.h index 1c33a73..0433a87 100644 --- a/include/lantern/core/client.h +++ b/include/lantern/core/client.h @@ -7,6 +7,7 @@ #include #include "lantern/consensus/state.h" +#include "lantern/consensus/store.h" #include "lantern/consensus/duties.h" #include "lantern/consensus/runtime.h" #include "lantern/consensus/fork_choice.h" @@ -126,18 +127,6 @@ struct lantern_active_blocks_request { bool timeout_recorded; }; -struct lantern_agg_proof_cache_entry { - LanternRoot data_root; - LanternAggregatedSignatureProof proof; - uint64_t target_slot; -}; - -struct lantern_agg_proof_cache { - struct lantern_agg_proof_cache_entry *entries; - size_t length; - size_t capacity; -}; - struct lantern_validator_duty_state { uint64_t last_slot; uint32_t last_interval; @@ -195,11 +184,11 @@ struct lantern_client { struct lantern_consensus_runtime runtime; bool has_runtime; struct lantern_validator_duty_state validator_duty; + LanternStore store; LanternForkChoice fork_choice; bool has_fork_choice; LanternState state; bool has_state; - struct lantern_agg_proof_cache agg_proof_cache; pthread_mutex_t state_lock; bool state_lock_initialized; bool *validator_enabled; @@ -210,6 +199,9 @@ struct lantern_client { size_t peer_vote_stats_cap; pthread_mutex_t peer_vote_lock; bool peer_vote_lock_initialized; + pthread_t timing_thread; + bool timing_thread_started; + int timing_stop_flag; pthread_t validator_thread; bool validator_thread_started; int validator_stop_flag; diff --git a/include/lantern/storage/storage.h b/include/lantern/storage/storage.h index 8ab4829..c955ebd 100644 --- a/include/lantern/storage/storage.h +++ b/include/lantern/storage/storage.h @@ -6,6 +6,7 @@ #include "lantern/consensus/containers.h" #include "lantern/consensus/state.h" +#include "lantern/consensus/store.h" #include "lantern/networking/messages.h" #ifdef __cplusplus @@ -22,8 +23,14 @@ int lantern_storage_save_state(const char *data_dir, const LanternState *state); int lantern_storage_load_state(const char *data_dir, LanternState *state); int lantern_storage_save_finalized_state(const char *data_dir, const LanternState *state); int lantern_storage_load_finalized_state(const char *data_dir, LanternState *state); -int lantern_storage_save_votes(const char *data_dir, const LanternState *state); -int lantern_storage_load_votes(const char *data_dir, LanternState *state); +int lantern_storage_save_votes( + const char *data_dir, + const LanternState *state, + const LanternStore *store); +int lantern_storage_load_votes( + const char *data_dir, + LanternState *state, + LanternStore *store); int lantern_storage_store_block(const char *data_dir, const LanternSignedBlock *block); int lantern_storage_store_block_for_root( const char *data_dir, diff --git a/src/consensus/fork_choice.c b/src/consensus/fork_choice.c index 18286b3..92ad36b 100644 --- a/src/consensus/fork_choice.c +++ b/src/consensus/fork_choice.c @@ -7,6 +7,7 @@ #include "lantern/consensus/hash.h" #include "lantern/consensus/quorum.h" +#include "lantern/consensus/store.h" #include "lantern/metrics/lean_metrics.h" #include "lantern/support/log.h" #include "lantern/support/strings.h" @@ -76,6 +77,23 @@ static void map_reset(LanternForkChoice *store) { store->index_len = 0; } +static void states_reset(LanternForkChoice *store) { + if (!store || !store->states) { + return; + } + for (size_t i = 0; i < store->state_cap; ++i) { + struct lantern_fork_choice_state_entry *entry = &store->states[i]; + if (!entry->has_state) { + continue; + } + lantern_state_reset(&entry->state); + entry->has_state = false; + } + free(store->states); + store->states = NULL; + store->state_cap = 0; +} + static int map_reserve(LanternForkChoice *store, size_t desired) { if (!store) { return -1; @@ -242,24 +260,7 @@ static int ensure_vote_tables_disjoint(LanternForkChoice *store) { if (!store || !store->known_votes || !store->new_votes) { return -1; } - if (store->known_votes != store->new_votes) { - return 0; - } - if (store->validator_count == 0) { - return -1; - } - if (store->validator_count > SIZE_MAX / sizeof(*store->new_votes)) { - return -1; - } - - size_t bytes = store->validator_count * sizeof(*store->new_votes); - struct lantern_fork_choice_vote_entry *copied = malloc(bytes); - if (!copied) { - return -1; - } - memcpy(copied, store->new_votes, bytes); - store->new_votes = copied; - return 0; + return store->known_votes != store->new_votes ? 0 : -1; } struct vote_undo_entry { @@ -349,6 +350,7 @@ static int ensure_block_capacity(LanternForkChoice *store, size_t required) { if (store->block_cap >= required) { return 0; } + size_t previous_cap = store->block_cap; size_t capacity = store->block_cap == 0 ? 4u : store->block_cap; while (capacity < required) { if (capacity > (SIZE_MAX / 2)) { @@ -363,7 +365,22 @@ static int ensure_block_capacity(LanternForkChoice *store, size_t required) { return -1; } store->blocks = entries; + if (capacity > previous_cap) { + memset(&store->blocks[previous_cap], 0, (capacity - previous_cap) * sizeof(*store->blocks)); + } + + struct lantern_fork_choice_state_entry *states = realloc( + store->states, + capacity * sizeof(*states)); + if (!states) { + return -1; + } + store->states = states; + if (capacity > store->state_cap) { + memset(&store->states[store->state_cap], 0, (capacity - store->state_cap) * sizeof(*store->states)); + } store->block_cap = capacity; + store->state_cap = capacity; return 0; } @@ -419,6 +436,16 @@ void lantern_fork_choice_reset(LanternForkChoice *store) { if (!store) { return; } + struct lantern_fork_choice_vote_entry *known_votes = store->known_votes; + struct lantern_fork_choice_vote_entry *new_votes = store->new_votes; + size_t validator_count = store->validator_count; + const struct lantern_aggregated_payload_pool *new_aggregated_payloads = + store->new_aggregated_payloads; + const struct lantern_aggregated_payload_pool *known_aggregated_payloads = + store->known_aggregated_payloads; + const struct lantern_attestation_data_by_root *attestation_data_by_root = + store->attestation_data_by_root; + states_reset(store); free(store->blocks); store->blocks = NULL; store->block_cap = 0; @@ -426,18 +453,6 @@ void lantern_fork_choice_reset(LanternForkChoice *store) { map_reset(store); - if (store->known_votes == store->new_votes) { - free(store->known_votes); - store->known_votes = NULL; - store->new_votes = NULL; - } else { - free(store->known_votes); - store->known_votes = NULL; - free(store->new_votes); - store->new_votes = NULL; - } - store->validator_count = 0; - store->initialized = false; store->has_anchor = false; store->has_head = false; @@ -451,6 +466,12 @@ void lantern_fork_choice_reset(LanternForkChoice *store) { store->intervals_per_slot = LANTERN_FORK_CHOICE_DEFAULT_INTERVALS_PER_SLOT; store->milliseconds_per_interval = ((uint64_t)store->seconds_per_slot * LANTERN_FORK_CHOICE_MILLISECONDS_PER_SECOND) / store->intervals_per_slot; + store->known_votes = known_votes; + store->new_votes = new_votes; + store->validator_count = validator_count; + store->new_aggregated_payloads = new_aggregated_payloads; + store->known_aggregated_payloads = known_aggregated_payloads; + store->attestation_data_by_root = attestation_data_by_root; } int lantern_fork_choice_configure(LanternForkChoice *store, const LanternConfig *config) { @@ -464,14 +485,10 @@ int lantern_fork_choice_configure(LanternForkChoice *store, const LanternConfig store->config = *config; store->validator_count = (size_t)config->num_validators; - store->known_votes = calloc(store->validator_count, sizeof(*store->known_votes)); - if (!store->known_votes) { - lantern_fork_choice_reset(store); + if (!store->known_votes || !store->new_votes) { return -1; } - store->new_votes = calloc(store->validator_count, sizeof(*store->new_votes)); - if (!store->new_votes) { - lantern_fork_choice_reset(store); + if (store->known_votes == store->new_votes) { return -1; } store->seconds_per_slot = LANTERN_FORK_CHOICE_DEFAULT_SECONDS_PER_SLOT; @@ -568,12 +585,76 @@ int lantern_fork_choice_set_block_validator_count( return 0; } +int lantern_fork_choice_set_block_state( + LanternForkChoice *store, + const LanternRoot *root, + const LanternState *state) { + if (!store || !root || !state) { + return -1; + } + size_t index = 0; + if (!map_lookup(store, root, &index)) { + return -1; + } + if (!store->states || index >= store->state_cap) { + return -1; + } + + LanternState cloned; + lantern_state_init(&cloned); + if (lantern_state_clone(state, &cloned) != 0) { + lantern_state_reset(&cloned); + return -1; + } + + struct lantern_fork_choice_state_entry *entry = &store->states[index]; + if (entry->has_state) { + lantern_state_reset(&entry->state); + } + entry->state = cloned; + entry->has_state = true; + return 0; +} + +const LanternState *lantern_fork_choice_block_state( + const LanternForkChoice *store, + const LanternRoot *root) { + if (!store || !root) { + return NULL; + } + size_t index = 0; + if (!map_lookup(store, root, &index)) { + return NULL; + } + if (!store->states || index >= store->state_cap) { + return NULL; + } + const struct lantern_fork_choice_state_entry *entry = &store->states[index]; + return entry->has_state ? &entry->state : NULL; +} + int lantern_fork_choice_set_anchor( LanternForkChoice *store, const LanternBlock *anchor_block, const LanternCheckpoint *latest_justified, const LanternCheckpoint *latest_finalized, const LanternRoot *block_root_hint) { + return lantern_fork_choice_set_anchor_with_state( + store, + anchor_block, + latest_justified, + latest_finalized, + block_root_hint, + NULL); +} + +int lantern_fork_choice_set_anchor_with_state( + LanternForkChoice *store, + const LanternBlock *anchor_block, + const LanternCheckpoint *latest_justified, + const LanternCheckpoint *latest_finalized, + const LanternRoot *block_root_hint, + const LanternState *anchor_state) { if (!store || !store->initialized || !anchor_block) { return -1; } @@ -585,6 +666,26 @@ int lantern_fork_choice_set_anchor( return -1; } } + size_t existing_index = 0; + bool existed = map_lookup(store, &root, &existing_index); + struct lantern_fork_choice_block_entry previous_entry; + memset(&previous_entry, 0, sizeof(previous_entry)); + if (existed) { + if (!store->blocks || existing_index >= store->block_len) { + return -1; + } + previous_entry = store->blocks[existing_index]; + } + size_t previous_block_len = store->block_len; + LanternCheckpoint previous_justified = store->latest_justified; + LanternCheckpoint previous_finalized = store->latest_finalized; + LanternRoot previous_head = store->head; + bool previous_has_head = store->has_head; + LanternRoot previous_safe_target = store->safe_target; + bool previous_has_safe_target = store->has_safe_target; + bool previous_has_anchor = store->has_anchor; + uint64_t previous_time_intervals = store->time_intervals; + if (register_block(store, &root, &anchor_block->parent_root, anchor_block->slot, latest_justified, latest_finalized) != 0) { return -1; } @@ -605,6 +706,30 @@ int lantern_fork_choice_set_anchor( store->has_anchor = true; uint64_t anchor_intervals = anchor_block->slot * store->intervals_per_slot; store->time_intervals = anchor_intervals; + if (anchor_state && lantern_fork_choice_set_block_state(store, &root, anchor_state) != 0) { + store->latest_justified = previous_justified; + store->latest_finalized = previous_finalized; + store->head = previous_head; + store->has_head = previous_has_head; + store->safe_target = previous_safe_target; + store->has_safe_target = previous_has_safe_target; + store->has_anchor = previous_has_anchor; + store->time_intervals = previous_time_intervals; + if (existed) { + store->blocks[existing_index] = previous_entry; + } else { + size_t new_index = store->block_len > 0 ? (store->block_len - 1u) : 0u; + for (size_t i = 0; i + 1u < store->block_len; ++i) { + struct lantern_fork_choice_block_entry *entry = &store->blocks[i]; + if (entry->parent_index == new_index && root_compare(&entry->parent_root, &root) == 0) { + entry->parent_index = SIZE_MAX; + } + } + map_remove(store, &root); + store->block_len = previous_block_len; + } + return -1; + } return 0; } @@ -633,49 +758,49 @@ static int update_global_checkpoints( } if (post_justified && !root_is_zero(&post_justified->root) - && post_justified->slot >= store->latest_justified.slot + && post_justified->slot > store->latest_justified.slot && checkpoint_known_in_store(store, post_justified)) { store->latest_justified = *post_justified; } if (post_finalized && !root_is_zero(&post_finalized->root) - && post_finalized->slot >= store->latest_finalized.slot + && post_finalized->slot > store->latest_finalized.slot && checkpoint_known_in_store(store, post_finalized)) { store->latest_finalized = *post_finalized; } return 0; } -static int lantern_fork_choice_stage_proposer_vote( +int lantern_fork_choice_add_block( LanternForkChoice *store, - const LanternSignedVote *vote, - uint64_t block_slot, - uint64_t proposer_index) { - if (!store) { - return -1; - } - if (!vote) { - return 0; - } - if (vote->data.slot != block_slot) { - return -1; - } - if (vote->data.validator_id != proposer_index) { - return -1; - } - return lantern_fork_choice_add_vote(store, vote, false); + const LanternBlock *block, + const LanternSignedVote *proposer_attestation, + const LanternCheckpoint *post_justified, + const LanternCheckpoint *post_finalized, + const LanternRoot *block_root_hint) { + return lantern_fork_choice_add_block_with_state( + store, + block, + proposer_attestation, + post_justified, + post_finalized, + block_root_hint, + NULL); } -int lantern_fork_choice_add_block( +int lantern_fork_choice_add_block_with_state( LanternForkChoice *store, const LanternBlock *block, const LanternSignedVote *proposer_attestation, const LanternCheckpoint *post_justified, const LanternCheckpoint *post_finalized, - const LanternRoot *block_root_hint) { + const LanternRoot *block_root_hint, + const LanternState *post_state) { if (!store || !store->initialized || !store->has_anchor || !block) { return -1; } + /* Proposer attestations are cached by the caller after head recomputation. */ + (void)proposer_attestation; bool trace_finalization = finalization_trace_enabled(); struct lantern_log_metadata trace_meta = {.has_slot = true, .slot = block->slot}; char block_hex[(LANTERN_ROOT_SIZE * 2u) + 3u]; @@ -758,20 +883,6 @@ int lantern_fork_choice_add_block( memset(&wrapped_vote, 0, sizeof(wrapped_vote)); wrapped_vote.data = expanded.data[i]; if (lantern_fork_choice_add_vote(store, &wrapped_vote, true) != 0) { - /* - * Block attestations should not make block import fail if they - * conflict with already-seen votes. Consensus state transition has - * already validated/skipped attestation effects; here we best-effort - * stage fork-choice votes. - */ - lantern_log_debug( - "forkchoice", - &trace_meta, - "skipping conflicting block vote validator=%" PRIu64 - " vote_slot=%" PRIu64 " target_slot=%" PRIu64, - wrapped_vote.data.validator_id, - wrapped_vote.data.slot, - wrapped_vote.data.target.slot); continue; } } @@ -779,25 +890,8 @@ int lantern_fork_choice_add_block( if (lantern_fork_choice_recompute_head(store) != 0) { goto rollback; } - - if (proposer_attestation) { - size_t proposer_index = (size_t)block->proposer_index; - if (vote_undo_save(&undo, touched, touched_bytes, store, proposer_index) != 0) { - goto rollback; - } - } - if (lantern_fork_choice_stage_proposer_vote(store, proposer_attestation, block->slot, block->proposer_index) - != 0) { - /* - * Proposer attestation is advisory for fork-choice vote tracking. - * Ignore conflicts to avoid dropping an otherwise valid block. - */ - lantern_log_debug( - "forkchoice", - &trace_meta, - "ignoring conflicting proposer vote proposer=%" PRIu64 " block_slot=%" PRIu64, - block->proposer_index, - block->slot); + if (post_state && lantern_fork_choice_set_block_state(store, &block_root, post_state) != 0) { + goto rollback; } lean_metrics_record_fork_choice_block_time(lantern_time_now_seconds() - metrics_start); if (trace_finalization) { @@ -878,10 +972,11 @@ int lantern_fork_choice_add_vote( size_t validator = (size_t)vote->data.validator_id; struct lantern_fork_choice_vote_entry *table = from_block ? store->known_votes : store->new_votes; struct lantern_fork_choice_vote_entry *entry = &table[validator]; - if (!entry->has_checkpoint || vote->data.slot > entry->checkpoint.slot) { + if (!entry->has_checkpoint || vote->data.slot > entry->slot) { entry->checkpoint = *head; + entry->slot = vote->data.slot; entry->has_checkpoint = true; - } else if (vote->data.slot == entry->checkpoint.slot) { + } else if (vote->data.slot == entry->slot) { if (root_compare(&entry->checkpoint.root, &head->root) != 0) { return -1; } @@ -889,7 +984,7 @@ int lantern_fork_choice_add_vote( if (from_block) { struct lantern_fork_choice_vote_entry *pending = &store->new_votes[validator]; - if (pending->has_checkpoint && pending->checkpoint.slot <= entry->checkpoint.slot) { + if (pending->has_checkpoint && pending->slot <= entry->slot) { pending->has_checkpoint = false; } } @@ -1072,6 +1167,142 @@ static int lmd_ghost_compute( } } +static void safe_target_consider_vote( + struct lantern_fork_choice_vote_entry *merged_votes, + uint64_t *latest_slots, + size_t vote_count, + size_t validator_index, + const LanternCheckpoint *head, + uint64_t attestation_slot) { + if (!merged_votes || !latest_slots || !head || validator_index >= vote_count) { + return; + } + struct lantern_fork_choice_vote_entry *merged = &merged_votes[validator_index]; + if (!merged->has_checkpoint || attestation_slot > latest_slots[validator_index]) { + merged->checkpoint = *head; + merged->has_checkpoint = true; + latest_slots[validator_index] = attestation_slot; + } +} + +static void safe_target_merge_vote_table( + const struct lantern_fork_choice_vote_entry *source_votes, + size_t vote_count, + struct lantern_fork_choice_vote_entry *merged_votes, + uint64_t *latest_slots) { + if (!source_votes || !merged_votes || !latest_slots) { + return; + } + for (size_t i = 0; i < vote_count; ++i) { + const struct lantern_fork_choice_vote_entry *vote = &source_votes[i]; + if (!vote->has_checkpoint) { + continue; + } + safe_target_consider_vote( + merged_votes, + latest_slots, + vote_count, + i, + &vote->checkpoint, + vote->checkpoint.slot); + } +} + +static const LanternAttestationData *safe_target_lookup_attestation_data( + const LanternForkChoice *store, + const LanternRoot *data_root) { + if (!store || !store->attestation_data_by_root || !data_root) { + return NULL; + } + const struct lantern_attestation_data_by_root *cache = store->attestation_data_by_root; + for (size_t i = 0; i < cache->length; ++i) { + if (memcmp(cache->entries[i].data_root.bytes, data_root->bytes, LANTERN_ROOT_SIZE) == 0) { + return &cache->entries[i].data; + } + } + return NULL; +} + +static void safe_target_merge_payload_pool( + const LanternForkChoice *store, + const struct lantern_aggregated_payload_pool *pool, + struct lantern_fork_choice_vote_entry *merged_votes, + uint64_t *latest_slots, + size_t vote_count) { + if (!store || !pool || !merged_votes || !latest_slots || !pool->entries) { + return; + } + for (size_t i = 0; i < pool->length; ++i) { + const struct lantern_aggregated_payload_entry *entry = &pool->entries[i]; + const LanternAttestationData *attestation_data = + safe_target_lookup_attestation_data(store, &entry->data_root); + if (!attestation_data) { + continue; + } + const struct lantern_bitlist *participants = &entry->proof.participants; + if (participants->bit_length == 0 || !participants->bytes) { + continue; + } + size_t limit = participants->bit_length; + if (limit > vote_count) { + limit = vote_count; + } + for (size_t validator = 0; validator < limit; ++validator) { + if (!lantern_bitlist_get(participants, validator)) { + continue; + } + safe_target_consider_vote( + merged_votes, + latest_slots, + vote_count, + validator, + &attestation_data->head, + attestation_data->slot); + } + } +} + +static bool fork_choice_has_attached_payload_views(const LanternForkChoice *store) { + return store + && store->new_aggregated_payloads + && store->known_aggregated_payloads + && store->attestation_data_by_root; +} + +static int materialize_attached_payload_votes(LanternForkChoice *store) { + if (!store || !fork_choice_has_attached_payload_views(store)) { + return 0; + } + + uint64_t *latest_slots = calloc(store->validator_count, sizeof(*latest_slots)); + if (!latest_slots) { + return -1; + } + + for (size_t i = 0; i < store->validator_count; ++i) { + if (!store->known_votes[i].has_checkpoint) { + continue; + } + latest_slots[i] = store->known_votes[i].slot; + } + + safe_target_merge_payload_pool( + store, + store->known_aggregated_payloads, + store->known_votes, + latest_slots, + store->validator_count); + safe_target_merge_payload_pool( + store, + store->new_aggregated_payloads, + store->known_votes, + latest_slots, + store->validator_count); + + free(latest_slots); + return 0; +} + static size_t fork_choice_reorg_depth( const LanternForkChoice *store, size_t old_index, @@ -1152,13 +1383,6 @@ int lantern_fork_choice_recompute_head(LanternForkChoice *store) { } } - size_t head_index = 0; - if (map_lookup(store, &head, &head_index)) { - const struct lantern_fork_choice_block_entry *entry = &store->blocks[head_index]; - if (entry->has_finalized) { - store->latest_finalized = entry->latest_finalized; - } - } return 0; } @@ -1180,6 +1404,9 @@ int lantern_fork_choice_accept_new_votes(LanternForkChoice *store) { } pending->has_checkpoint = false; } + if (materialize_attached_payload_votes(store) != 0) { + return -1; + } return lantern_fork_choice_recompute_head(store); } @@ -1198,17 +1425,46 @@ int lantern_fork_choice_update_safe_target(LanternForkChoice *store) { } } uint64_t threshold = lantern_consensus_quorum_threshold(validator_count); + struct lantern_fork_choice_vote_entry *merged_votes = + calloc(store->validator_count, sizeof(*merged_votes)); + uint64_t *latest_slots = calloc(store->validator_count, sizeof(*latest_slots)); + if (!merged_votes || !latest_slots) { + free(merged_votes); + free(latest_slots); + return -1; + } + if (fork_choice_has_attached_payload_views(store)) { + safe_target_merge_payload_pool( + store, + store->known_aggregated_payloads, + merged_votes, + latest_slots, + store->validator_count); + safe_target_merge_payload_pool( + store, + store->new_aggregated_payloads, + merged_votes, + latest_slots, + store->validator_count); + } else { + safe_target_merge_vote_table(store->known_votes, store->validator_count, merged_votes, latest_slots); + safe_target_merge_vote_table(store->new_votes, store->validator_count, merged_votes, latest_slots); + } LanternRoot safe; if (lmd_ghost_compute( store, &store->latest_justified.root, - store->new_votes, + merged_votes, store->validator_count, threshold, &safe) != 0) { + free(merged_votes); + free(latest_slots); return -1; } + free(merged_votes); + free(latest_slots); store->safe_target = safe; store->has_safe_target = true; return 0; diff --git a/src/consensus/hash.c b/src/consensus/hash.c index eed6fcb..e01fc92 100644 --- a/src/consensus/hash.c +++ b/src/consensus/hash.c @@ -927,7 +927,8 @@ int lantern_hash_tree_root_state(const LanternState *state, LanternRoot *out_roo LanternRoot justified_slots_root; LanternRoot justification_roots_root; LanternRoot justification_validators_root; - LanternRoot validators_root = state->validator_registry_root; + LanternRoot validators_root; + memset(&validators_root, 0, sizeof(validators_root)); if (lantern_hash_tree_root_config(&state->config, &config_root) != 0) { return -1; @@ -968,6 +969,41 @@ int lantern_hash_tree_root_state(const LanternState *state, LanternRoot *out_roo &justification_validators_root) != 0) { return -1; } + if (state->validator_count == 0) { + if (lantern_hash_tree_root_validators(NULL, 0, &validators_root) != 0) { + return -1; + } + } else { + if (!state->validators || state->validator_count > SIZE_MAX / SSZ_BYTES_PER_CHUNK) { + return -1; + } + uint8_t *validator_chunks = malloc(state->validator_count * SSZ_BYTES_PER_CHUNK); + if (!validator_chunks) { + return -1; + } + for (size_t i = 0; i < state->validator_count; ++i) { + LanternRoot validator_root; + if (hash_validator(state->validators[i].pubkey, state->validators[i].index, &validator_root) != 0) { + free(validator_chunks); + return -1; + } + memcpy( + validator_chunks + (i * SSZ_BYTES_PER_CHUNK), + validator_root.bytes, + SSZ_BYTES_PER_CHUNK); + } + uint8_t temp_root[SSZ_BYTES_PER_CHUNK]; + ssz_error_t validator_err = + ssz_merkleize(validator_chunks, state->validator_count, LANTERN_VALIDATOR_REGISTRY_LIMIT, temp_root); + free(validator_chunks); + if (validator_err != SSZ_SUCCESS) { + return -1; + } + validator_err = ssz_mix_in_length(temp_root, (uint64_t)state->validator_count, validators_root.bytes); + if (validator_err != SSZ_SUCCESS) { + return -1; + } + } uint8_t chunks[10][SSZ_BYTES_PER_CHUNK]; memcpy(chunks[0], config_root.bytes, SSZ_BYTES_PER_CHUNK); diff --git a/src/consensus/ssz.c b/src/consensus/ssz.c index 100372b..7354b53 100644 --- a/src/consensus/ssz.c +++ b/src/consensus/ssz.c @@ -2075,8 +2075,5 @@ int lantern_ssz_decode_state(LanternState *state, const uint8_t *data, size_t da } state->config.num_validators = (uint64_t)state->validator_count; - if (state->latest_finalized.slot != UINT64_MAX) { - state->justified_slots_offset = state->latest_finalized.slot + 1u; - } return 0; } diff --git a/src/consensus/state.c b/src/consensus/state.c index 86272fd..112571e 100644 --- a/src/consensus/state.c +++ b/src/consensus/state.c @@ -16,13 +16,8 @@ #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/quorum.h" - -struct lantern_vote_record { - LanternVote vote; - LanternSignature signature; - bool has_vote; - bool has_signature; -}; +#include "lantern/consensus/signature.h" +#include "lantern/consensus/store.h" static void record_attestation_validation_metric(double start_seconds, bool valid) { lean_metrics_record_attestation_validation(lantern_time_now_seconds() - start_seconds, valid); @@ -45,10 +40,16 @@ static bool finalization_trace_enabled(void) { return false; } +static uint64_t lantern_state_justified_slots_anchor(const LanternState *state) { + if (!state || state->latest_finalized.slot == UINT64_MAX) { + return 0u; + } + return state->latest_finalized.slot + 1u; +} + 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); -static int lantern_root_list_drop_front(struct lantern_root_list *list, size_t count); static int lantern_bitlist_set_bit(struct lantern_bitlist *list, size_t index, bool value); static int lantern_bitlist_get_bit(const struct lantern_bitlist *list, size_t index, bool *out_value); static int lantern_bitlist_ensure_length(struct lantern_bitlist *list, size_t bit_length); @@ -59,13 +60,19 @@ 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); static int collect_attestations_for_checkpoint( const LanternState *state, + const LanternStore *vote_store, + const LanternStore *proof_store, const LanternCheckpoint *checkpoint, LanternAttestations *out_attestations, LanternSignatureList *out_signatures); static int lantern_state_process_attestations_internal( LanternState *state, + LanternStore *store, const LanternAttestations *attestations, const LanternSignatureList *signatures, bool apply_consensus_effects); @@ -82,6 +89,262 @@ static bool signature_is_zero(const LanternSignature *signature) { return true; } +static int lantern_state_cache_proposer_attestation( + const LanternState *state, + LanternStore *store, + const LanternSignedVote *proposer_attestation) { + if (!state || !store) { + return -1; + } + if (!proposer_attestation) { + return 0; + } + + 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; + } + + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote->data, &data_root) != 0) { + return -1; + } + + LanternSignatureKey key = { + .validator_index = vote->validator_id, + .data_root = data_root, + }; + return lantern_store_set_gossip_signature( + store, + &key, + &vote->data, + &proposer_attestation->signature, + vote->target.slot); +} + +static bool validator_pubkey_is_zero(const uint8_t *pubkey) { + if (!pubkey) { + return true; + } + for (size_t i = 0; i < LANTERN_VALIDATOR_PUBKEY_SIZE; ++i) { + if (pubkey[i] != 0u) { + return false; + } + } + return true; +} + +static int lantern_state_verify_block_signatures( + const LanternState *state, + const LanternBlock *block, + const LanternBlockSignatures *signatures, + const LanternSignedVote *proposer_attestation) { + if (!state || !block || !signatures) { + return -1; + } + + const struct lantern_log_metadata meta = { + .has_slot = true, + .slot = block->slot, + }; + const LanternAggregatedAttestations *attestations = &block->body.attestations; + const LanternAttestationSignatures *sig_groups = &signatures->attestation_signatures; + size_t attestation_count = attestations->length; + size_t validator_count = lantern_state_validator_count(state); + + if (attestation_count != sig_groups->length) { + lantern_log_warn( + "state", + &meta, + "block signature count mismatch expected=%zu actual=%zu", + attestation_count, + sig_groups->length); + return -1; + } + if (attestation_count > 0 && (!attestations->data || !sig_groups->data)) { + lantern_log_warn("state", &meta, "block attestation/signature data missing"); + return -1; + } + + for (size_t i = 0; i < attestation_count; ++i) { + const LanternAggregatedAttestation *attestation = &attestations->data[i]; + const LanternAggregatedSignatureProof *proof = &sig_groups->data[i]; + size_t bit_length = attestation->aggregation_bits.bit_length; + size_t participant_count = 0; + + if (proof->participants.bit_length != bit_length) { + lantern_log_warn( + "state", + &meta, + "attestation signature participant length mismatch index=%zu expected=%zu actual=%zu", + i, + bit_length, + proof->participants.bit_length); + return -1; + } + if (bit_length > 0 && !attestation->aggregation_bits.bytes) { + lantern_log_warn( + "state", + &meta, + "attestation aggregation bits missing index=%zu bit_length=%zu", + i, + bit_length); + return -1; + } + + size_t bytes = (bit_length + 7u) / 8u; + if (bytes > 0) { + if (!proof->participants.bytes + || memcmp(proof->participants.bytes, attestation->aggregation_bits.bytes, bytes) != 0) { + lantern_log_warn( + "state", + &meta, + "attestation signature participants mismatch index=%zu", + i); + return -1; + } + } + + for (size_t v = 0; v < bit_length; ++v) { + if (!lantern_bitlist_get(&attestation->aggregation_bits, v)) { + continue; + } + if (v >= validator_count) { + lantern_log_warn( + "state", + &meta, + "attestation participant out of range index=%zu validator=%zu validators=%zu", + i, + v, + validator_count); + return -1; + } + participant_count += 1u; + } + if (participant_count == 0) { + lantern_log_warn( + "state", + &meta, + "attestation signature has no participants index=%zu", + i); + return -1; + } + + const uint8_t **pubkeys = calloc(participant_count, sizeof(*pubkeys)); + if (!pubkeys) { + return -1; + } + + int rc = 0; + size_t pubkey_index = 0; + for (size_t v = 0; v < bit_length; ++v) { + if (!lantern_bitlist_get(&attestation->aggregation_bits, v)) { + continue; + } + const uint8_t *pubkey = lantern_state_validator_pubkey(state, v); + if (!pubkey || validator_pubkey_is_zero(pubkey)) { + lantern_log_warn( + "state", + &meta, + "attestation participant pubkey missing index=%zu validator=%zu", + i, + v); + rc = -1; + break; + } + pubkeys[pubkey_index++] = pubkey; + } + + LanternRoot data_root; + if (rc == 0 + && lantern_hash_tree_root_attestation_data(&attestation->data, &data_root) != 0) { + lantern_log_warn( + "state", + &meta, + "failed to hash attestation for signature verification index=%zu", + i); + rc = -1; + } + if (rc == 0 + && !lantern_signature_verify_aggregated( + pubkeys, + participant_count, + &data_root, + &proof->proof_data, + attestation->data.slot)) { + lantern_log_warn( + "state", + &meta, + "invalid aggregated attestation signature index=%zu", + i); + rc = -1; + } + + free(pubkeys); + if (rc != 0) { + return -1; + } + } + + if (!proposer_attestation) { + return 0; + } + if (proposer_attestation->data.validator_id != block->proposer_index) { + lantern_log_warn( + "state", + &meta, + "proposer attestation validator mismatch expected=%" PRIu64 " actual=%" PRIu64, + block->proposer_index, + proposer_attestation->data.validator_id); + return -1; + } + if (signature_is_zero(&signatures->proposer_signature)) { + lantern_log_warn("state", &meta, "missing proposer signature"); + return -1; + } + if (proposer_attestation->data.validator_id >= validator_count) { + lantern_log_warn( + "state", + &meta, + "proposer index out of range validator=%" PRIu64 " validators=%zu", + proposer_attestation->data.validator_id, + validator_count); + return -1; + } + + const uint8_t *proposer_pubkey = + lantern_state_validator_pubkey(state, (size_t)proposer_attestation->data.validator_id); + if (!proposer_pubkey || validator_pubkey_is_zero(proposer_pubkey)) { + lantern_log_warn("state", &meta, "missing proposer pubkey"); + return -1; + } + + LanternRoot proposer_root; + if (lantern_hash_tree_root_attestation_data(&proposer_attestation->data.data, &proposer_root) != 0) { + lantern_log_warn("state", &meta, "failed to hash proposer attestation"); + return -1; + } + if (!lantern_signature_verify( + proposer_pubkey, + LANTERN_VALIDATOR_PUBKEY_SIZE, + proposer_attestation->data.slot, + &signatures->proposer_signature, + &proposer_root)) { + lantern_log_warn( + "state", + &meta, + "invalid proposer signature validator=%" PRIu64 " slot=%" PRIu64, + proposer_attestation->data.validator_id, + proposer_attestation->data.slot); + return -1; + } + + return 0; +} + static bool attestation_list_contains_validator(const LanternAttestations *list, uint64_t validator_id) { if (!list || !list->data || list->length == 0) { return false; @@ -94,19 +357,55 @@ 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; + } + + const struct lantern_aggregated_payload_pool *payloads = &store->known_aggregated_payloads; + if (!payloads->entries || payloads->length == 0) { + return false; + } + + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote->data, &data_root) != 0) { + return false; + } + + 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; + } + } + + return false; +} + static int collect_attestations_for_checkpoint( const LanternState *state, + const LanternStore *vote_store, + const LanternStore *proof_store, const LanternCheckpoint *checkpoint, LanternAttestations *out_attestations, LanternSignatureList *out_signatures) { - if (!state || !checkpoint || !out_attestations || !out_signatures) { + if (!state || !vote_store || !proof_store || !checkpoint || !out_attestations || !out_signatures) { return -1; } - if (!state->validator_votes || state->validator_votes_len == 0) { + if (!vote_store->validator_votes || vote_store->validator_votes_len == 0) { return 0; } - for (size_t i = 0; i < state->validator_votes_len; ++i) { - const struct lantern_vote_record *record = &state->validator_votes[i]; + 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) { continue; } @@ -116,6 +415,9 @@ static int collect_attestations_for_checkpoint( if (attestation_list_contains_validator(out_attestations, record->vote.validator_id)) { continue; } + if (!store_has_known_aggregated_payload_for_vote(proof_store, &record->vote)) { + continue; + } if (out_attestations->length >= LANTERN_MAX_ATTESTATIONS) { (void)lantern_attestations_resize(out_attestations, 0); (void)lantern_signature_list_resize(out_signatures, 0); @@ -275,9 +577,6 @@ int lantern_state_clone(const LanternState *source, LanternState *dest) { dest->latest_block_header = source->latest_block_header; dest->latest_justified = source->latest_justified; dest->latest_finalized = source->latest_finalized; - dest->validator_registry_root = source->validator_registry_root; - dest->historical_roots_offset = source->historical_roots_offset; - dest->justified_slots_offset = source->justified_slots_offset; if (clone_root_list(&dest->historical_block_hashes, &source->historical_block_hashes) != 0) { goto error; @@ -292,16 +591,6 @@ int lantern_state_clone(const LanternState *source, LanternState *dest) { goto error; } - if (source->validator_votes && source->validator_votes_len > 0) { - size_t len = source->validator_votes_len; - struct lantern_vote_record *records = malloc(len * sizeof(*records)); - if (!records) { - goto error; - } - memcpy(records, source->validator_votes, len * sizeof(*records)); - dest->validator_votes = records; - dest->validator_votes_len = len; - } if (source->validators && source->validator_count > 0) { size_t bytes = source->validator_count * sizeof(*source->validators); LanternValidator *validators = malloc(bytes); @@ -313,7 +602,6 @@ int lantern_state_clone(const LanternState *source, LanternState *dest) { dest->validator_count = source->validator_count; dest->validator_capacity = source->validator_count; } - dest->fork_choice = NULL; return 0; error: @@ -437,20 +725,6 @@ static int lantern_root_list_append(struct lantern_root_list *list, const Lanter return 0; } -static int lantern_root_list_drop_front(struct lantern_root_list *list, size_t count) { - if (!list || count == 0) { - return 0; - } - if (count >= list->length) { - return lantern_root_list_resize(list, 0); - } - size_t remaining = list->length - count; - memmove(list->items, list->items + count, remaining * sizeof(*list->items)); - memset(list->items + remaining, 0, count * sizeof(*list->items)); - list->length = remaining; - return 0; -} - static int lantern_bitlist_set_bit(struct lantern_bitlist *list, size_t index, bool value) { if (!list) { return -1; @@ -530,13 +804,15 @@ bool lantern_state_slot_in_justified_window(const LanternState *state, uint64_t if (!state) { return false; } - uint64_t offset = state->justified_slots_offset; - if (slot < offset) { + uint64_t anchor = lantern_state_justified_slots_anchor(state); + if (slot < anchor) { return true; } uint64_t bit_length = state->justified_slots.bit_length; - uint64_t window_end = offset + bit_length; - return slot >= offset && slot < window_end; + if (anchor > UINT64_MAX - bit_length) { + return false; + } + return slot < anchor + bit_length; } /** @@ -569,8 +845,8 @@ int lantern_state_get_justified_slot_bit(const LanternState *state, uint64_t slo if (!state || !out_value) { return -1; } - uint64_t offset = state->justified_slots_offset; - if (slot < offset) { + uint64_t anchor = lantern_state_justified_slots_anchor(state); + if (slot < anchor) { *out_value = true; return 0; } @@ -580,13 +856,13 @@ int lantern_state_get_justified_slot_bit(const LanternState *state, uint64_t slo lantern_log_debug( "state", &(const struct lantern_log_metadata){.has_slot = true, .slot = state->slot}, - "justification trace read slot=%" PRIu64 " value=false (outside window offset=%" PRIu64 ")", + "justification trace read slot=%" PRIu64 " value=false (outside window anchor=%" PRIu64 ")", slot, - state->justified_slots_offset); + anchor); } return 0; } - uint64_t relative = slot - offset; + uint64_t relative = slot - anchor; if (relative > SIZE_MAX) { return -1; } @@ -595,10 +871,10 @@ int lantern_state_get_justified_slot_bit(const LanternState *state, uint64_t slo lantern_log_debug( "state", &(const struct lantern_log_metadata){.has_slot = true, .slot = state->slot}, - "justification trace read slot=%" PRIu64 " value=%s offset=%" PRIu64, + "justification trace read slot=%" PRIu64 " value=%s anchor=%" PRIu64, slot, *out_value ? "true" : "false", - state->justified_slots_offset); + anchor); } return rc; } @@ -611,22 +887,13 @@ static int lantern_state_ensure_justified_slot_index(LanternState *state, uint64 if (limit == 0) { return -1; } - uint64_t offset = state->justified_slots_offset; - if (slot < offset) { + uint64_t anchor = lantern_state_justified_slots_anchor(state); + if (slot < anchor) { return 1; } - uint64_t relative = slot - offset; + uint64_t relative = slot - anchor; if (relative >= limit) { - uint64_t drop = (relative - limit) + 1u; - if (drop > SIZE_MAX) { - return -1; - } - if (lantern_bitlist_drop_front(&state->justified_slots, (size_t)drop) != 0) { - return -1; - } - state->justified_slots_offset += drop; - offset += drop; - relative = slot >= offset ? slot - offset : 0; + return -1; } if (relative > SIZE_MAX) { return -1; @@ -647,7 +914,7 @@ static int lantern_state_set_justified_slot_bit(LanternState *state, uint64_t sl if (!state) { return -1; } - if (slot < state->justified_slots_offset) { + if (slot < lantern_state_justified_slots_anchor(state)) { return 0; } if (slot > SIZE_MAX) { @@ -669,13 +936,7 @@ static int lantern_state_append_historical_root(LanternState *state, const Lante return -1; } if (state->historical_block_hashes.length >= LANTERN_HISTORICAL_ROOTS_LIMIT) { - if (state->historical_block_hashes.length == 0) { - return -1; - } - if (lantern_root_list_drop_front(&state->historical_block_hashes, 1) != 0) { - return -1; - } - state->historical_roots_offset += 1u; + return 0; } return lantern_root_list_append(&state->historical_block_hashes, root); } @@ -699,13 +960,6 @@ static int lantern_bitlist_ensure_length(struct lantern_bitlist *list, size_t bi return 0; } -static void lantern_vote_record_reset(struct lantern_vote_record *record) { - if (!record) { - return; - } - memset(record, 0, sizeof(*record)); -} - static bool lantern_checkpoint_equal(const LanternCheckpoint *a, const LanternCheckpoint *b) { if (!a || !b) { return false; @@ -716,25 +970,6 @@ static bool lantern_checkpoint_equal(const LanternCheckpoint *a, const LanternCh return memcmp(a->root.bytes, b->root.bytes, LANTERN_ROOT_SIZE) == 0; } -static bool lantern_votes_equal(const LanternVote *a, const LanternVote *b) { - if (!a || !b) { - return false; - } - if (a->slot != b->slot) { - return false; - } - if (!lantern_checkpoint_equal(&a->head, &b->head)) { - return false; - } - if (!lantern_checkpoint_equal(&a->target, &b->target)) { - return false; - } - if (!lantern_checkpoint_equal(&a->source, &b->source)) { - return false; - } - return true; -} - static size_t lantern_quorum_threshold(uint64_t validator_count) { uint64_t threshold = lantern_consensus_quorum_threshold(validator_count); if (threshold > SIZE_MAX) { @@ -975,25 +1210,12 @@ static int lantern_state_find_latest_slot_for_root( if (length == 0) { return 1; } - uint64_t offset = state->historical_roots_offset; - uint64_t end_slot = offset + (uint64_t)length - 1u; - if (start_slot > end_slot) { + if (start_slot >= length) { return 1; } - size_t start_index = 0; - if (start_slot > offset) { - uint64_t diff = start_slot - offset; - if (diff > SIZE_MAX) { - return -1; - } - if ((size_t)diff >= length) { - return 1; - } - start_index = (size_t)diff; - } - for (size_t i = length; i-- > start_index;) { + for (size_t i = length; i-- > (size_t)start_slot;) { if (memcmp(state->historical_block_hashes.items[i].bytes, root->bytes, LANTERN_ROOT_SIZE) == 0) { - *out_slot = offset + (uint64_t)i; + *out_slot = (uint64_t)i; return 0; } } @@ -1048,127 +1270,6 @@ static int lantern_state_prune_justification_roots( return 0; } -int lantern_state_prepare_validator_votes(LanternState *state, uint64_t validator_count) { - if (!state || validator_count == 0) { - return -1; - } - if (validator_count > (uint64_t)LANTERN_VALIDATOR_REGISTRY_LIMIT) { - return -1; - } - if (validator_count > SIZE_MAX) { - return -1; - } - size_t count = (size_t)validator_count; - if (state->validator_votes && state->validator_votes_len != count) { - free(state->validator_votes); - state->validator_votes = NULL; - state->validator_votes_len = 0; - } - if (!state->validator_votes) { - struct lantern_vote_record *records = calloc(count, sizeof(*records)); - if (!records) { - return -1; - } - state->validator_votes = records; - state->validator_votes_len = count; - } else { - for (size_t i = 0; i < count; ++i) { - lantern_vote_record_reset(&state->validator_votes[i]); - } - } - return 0; -} - -size_t lantern_state_validator_capacity(const LanternState *state) { - if (!state || !state->validator_votes) { - return 0; - } - return state->validator_votes_len; -} - -bool lantern_state_validator_has_vote(const LanternState *state, size_t index) { - if (!state || !state->validator_votes || index >= state->validator_votes_len) { - return false; - } - return state->validator_votes[index].has_vote; -} - -int lantern_state_get_signed_validator_vote( - const LanternState *state, - size_t index, - LanternSignedVote *out_vote) { - if (!state || !state->validator_votes || index >= state->validator_votes_len || !out_vote) { - return -1; - } - const struct lantern_vote_record *record = &state->validator_votes[index]; - if (!record->has_vote) { - return -1; - } - memset(out_vote, 0, sizeof(*out_vote)); - out_vote->data = record->vote; - out_vote->data.validator_id = (uint64_t)index; - if (record->has_signature) { - out_vote->signature = record->signature; - } - return 0; -} - -int lantern_state_get_validator_vote(const LanternState *state, size_t index, LanternVote *out_vote) { - if (!out_vote) { - return -1; - } - LanternSignedVote signed_vote; - if (lantern_state_get_signed_validator_vote(state, index, &signed_vote) != 0) { - return -1; - } - *out_vote = signed_vote.data; - return 0; -} - -int lantern_state_set_signed_validator_vote( - LanternState *state, - size_t index, - const LanternSignedVote *vote) { - if (!state || !state->validator_votes || index >= state->validator_votes_len || !vote) { - return -1; - } - struct lantern_vote_record *record = &state->validator_votes[index]; - LanternVote previous_vote = record->vote; - LanternSignature previous_signature = record->signature; - bool previous_has_signature = record->has_signature; - record->vote = vote->data; - record->vote.validator_id = (uint64_t)index; - record->has_vote = true; - if (!signature_is_zero(&vote->signature)) { - record->signature = vote->signature; - record->has_signature = true; - } else if (previous_has_signature && lantern_votes_equal(&previous_vote, &record->vote)) { - record->signature = previous_signature; - record->has_signature = true; - } else { - memset(&record->signature, 0, sizeof(record->signature)); - record->has_signature = false; - } - return 0; -} - -int lantern_state_set_validator_vote(LanternState *state, size_t index, const LanternVote *vote) { - if (!vote) { - return -1; - } - LanternSignedVote signed_vote; - memset(&signed_vote, 0, sizeof(signed_vote)); - signed_vote.data = *vote; - return lantern_state_set_signed_validator_vote(state, index, &signed_vote); -} - -void lantern_state_clear_validator_vote(LanternState *state, size_t index) { - if (!state || !state->validator_votes || index >= state->validator_votes_len) { - return; - } - lantern_vote_record_reset(&state->validator_votes[index]); -} - int lantern_state_set_validator_pubkeys(LanternState *state, const uint8_t *pubkeys, size_t count) { if (!state) { return -1; @@ -1182,13 +1283,6 @@ int lantern_state_set_validator_pubkeys(LanternState *state, const uint8_t *pubk if (count > 0 && count > SIZE_MAX / sizeof(*state->validators)) { return -1; } - LanternRoot registry_root; - lantern_root_zero(®istry_root); - if (count > 0) { - if (lantern_hash_tree_root_validators(pubkeys, count, ®istry_root) != 0) { - return -1; - } - } LanternValidator *items = NULL; if (count > 0) { items = malloc(count * sizeof(*items)); @@ -1209,7 +1303,6 @@ int lantern_state_set_validator_pubkeys(LanternState *state, const uint8_t *pubk state->validators = items; state->validator_count = count; state->validator_capacity = count; - state->validator_registry_root = registry_root; return 0; } @@ -1248,16 +1341,10 @@ void lantern_state_reset(LanternState *state) { if (!state) { return; } - struct lantern_fork_choice *attached = state->fork_choice; lantern_root_list_reset(&state->historical_block_hashes); lantern_bitlist_reset(&state->justified_slots); lantern_root_list_reset(&state->justification_roots); lantern_bitlist_reset(&state->justification_validators); - if (state->validator_votes) { - free(state->validator_votes); - state->validator_votes = NULL; - state->validator_votes_len = 0; - } if (state->validators) { free(state->validators); state->validators = NULL; @@ -1269,14 +1356,6 @@ void lantern_state_reset(LanternState *state) { lantern_bitlist_init(&state->justified_slots); lantern_root_list_init(&state->justification_roots); lantern_bitlist_init(&state->justification_validators); - state->fork_choice = attached; -} - -void lantern_state_attach_fork_choice(LanternState *state, struct lantern_fork_choice *fork_choice) { - if (!state) { - return; - } - state->fork_choice = fork_choice; } int lantern_state_generate_genesis(LanternState *state, uint64_t genesis_time, uint64_t num_validators) { @@ -1287,10 +1366,6 @@ int lantern_state_generate_genesis(LanternState *state, uint64_t genesis_time, u return -1; } lantern_state_reset(state); - if (lantern_state_prepare_validator_votes(state, num_validators) != 0) { - lantern_state_reset(state); - return -1; - } state->config.num_validators = num_validators; state->config.genesis_time = genesis_time; state->slot = 0; @@ -1315,14 +1390,6 @@ int lantern_state_generate_genesis(LanternState *state, uint64_t genesis_time, u state->latest_justified.slot = 0; lantern_root_zero(&state->latest_finalized.root); state->latest_finalized.slot = 0; - if (state->latest_finalized.slot != UINT64_MAX) { - /* - * Match LeanSpec: justified_slots is indexed starting at - * (latest_finalized.slot + 1), so finalized slots are implicitly - * justified and not explicitly represented in the bitlist. - */ - state->justified_slots_offset = state->latest_finalized.slot + 1u; - } return 0; } @@ -1394,9 +1461,9 @@ int lantern_state_mark_justified_slot(LanternState *state, uint64_t slot) { lantern_log_debug( "state", &(const struct lantern_log_metadata){.has_slot = true, .slot = state->slot}, - "justification trace mark slot=%" PRIu64 " offset=%" PRIu64 " window=%zu", + "justification trace mark slot=%" PRIu64 " anchor=%" PRIu64 " window=%zu", slot, - state->justified_slots_offset, + lantern_state_justified_slots_anchor(state), state->justified_slots.bit_length); } return rc; @@ -1521,10 +1588,11 @@ int lantern_state_process_block_header(LanternState *state, const LanternBlock * static int lantern_state_process_attestations_internal( LanternState *state, + LanternStore *store, const LanternAttestations *attestations, const LanternSignatureList *signatures, bool apply_consensus_effects) { - if (!state || !attestations) { + if (!state || !store || !attestations) { return -1; } uint64_t validator_count_u64 = state->config.num_validators; @@ -1537,7 +1605,7 @@ static int lantern_state_process_attestations_internal( .has_slot = true, .slot = state->slot, }; - if (!state->validator_votes || state->validator_votes_len != validator_count) { + if (!store->validator_votes || store->validator_votes_len != validator_count) { return -1; } @@ -1603,8 +1671,8 @@ static int lantern_state_process_attestations_internal( "finalization trace skip source_outside_window source_slot=%" PRIu64 " window=[%" PRIu64 ",%" PRIu64 ")", vote->source.slot, - state->justified_slots_offset, - state->justified_slots_offset + state->justified_slots.bit_length); + lantern_state_justified_slots_anchor(state), + lantern_state_justified_slots_anchor(state) + state->justified_slots.bit_length); } continue; } @@ -1708,7 +1776,7 @@ static int lantern_state_process_attestations_internal( if (signature) { stored_vote.signature = *signature; } - if (lantern_state_set_signed_validator_vote(state, (size_t)vote->validator_id, &stored_vote) != 0) { + if (lantern_store_set_signed_validator_vote(store, (size_t)vote->validator_id, &stored_vote) != 0) { record_attestation_validation_metric(att_validation_start, false); return -1; } @@ -1877,16 +1945,8 @@ static int lantern_state_process_attestations_internal( record_attestation_validation_metric(att_validation_start, false); return -1; } - if (state->justified_slots_offset <= UINT64_MAX - delta) { - state->justified_slots_offset += delta; - } else { - if (finalization_attempted) { - lean_metrics_record_finalization_attempt(false); - } - record_attestation_validation_metric(att_validation_start, false); - return -1; - } } + state->latest_finalized = latest_finalized; if (lantern_state_prune_justification_roots( state, base_finalized_slot, @@ -1939,9 +1999,9 @@ static int lantern_state_process_attestations_internal( state->latest_finalized.slot, state->latest_justified.slot); } - if (state->fork_choice) { + if (store->fork_choice) { if (lantern_fork_choice_update_checkpoints( - state->fork_choice, + store->fork_choice, &state->latest_justified, &state->latest_finalized) != 0) { @@ -1958,31 +2018,34 @@ static int lantern_state_process_attestations_internal( int lantern_state_process_attestations( LanternState *state, + LanternStore *store, const LanternAttestations *attestations, const LanternSignatureList *signatures) { - return lantern_state_process_attestations_internal(state, attestations, signatures, true); -} - -static int lantern_state_stage_attestations( - LanternState *state, - const LanternAttestations *attestations, - const LanternSignatureList *signatures) { - return lantern_state_process_attestations_internal(state, attestations, signatures, false); + return lantern_state_process_attestations_internal(state, store, attestations, signatures, true); } int lantern_state_process_block( LanternState *state, + LanternStore *store, const LanternBlock *block, const LanternBlockSignatures *signatures, const LanternSignedVote *proposer_attestation) { - if (!state || !block) { + if (!state || !store || !block) { return -1; } double block_metrics_start = lantern_time_now_seconds(); + if (signatures + && lantern_state_verify_block_signatures( + state, + block, + signatures, + proposer_attestation) + != 0) { + return -1; + } if (lantern_state_process_block_header(state, block) != 0) { return -1; } - (void)signatures; size_t validator_count = 0; if (state->config.num_validators > SIZE_MAX) { return -1; @@ -1994,41 +2057,18 @@ int lantern_state_process_block( lantern_attestations_reset(&expanded); return -1; } - if (lantern_state_process_attestations(state, &expanded, NULL) != 0) { + if (lantern_state_process_attestations(state, store, &expanded, NULL) != 0) { lantern_attestations_reset(&expanded); return -1; } lantern_attestations_reset(&expanded); - if (proposer_attestation && state->config.num_validators > 0) { - const LanternVote *vote = &proposer_attestation->data; - uint64_t validator_id = vote->validator_id; - if (validator_id < state->config.num_validators && vote->target.slot > vote->source.slot) { - LanternVote proposer_vote = *vote; - LanternAttestations proposer_batch = { - .data = &proposer_vote, - .length = 1, - .capacity = 1, - }; - LanternSignature proposer_sig = proposer_attestation->signature; - LanternSignatureList proposer_sig_batch = { - .data = &proposer_sig, - .length = 1, - .capacity = 1, - }; - const LanternSignatureList *sig_view = signature_is_zero(&proposer_sig) ? NULL : &proposer_sig_batch; - if (lantern_state_stage_attestations(state, &proposer_batch, sig_view) != 0) { - return -1; - } - } - } - lean_metrics_record_state_transition_block(lantern_time_now_seconds() - block_metrics_start); return 0; } -int lantern_state_transition(LanternState *state, const LanternSignedBlock *signed_block) { - if (!state || !signed_block) { +int lantern_state_transition(LanternState *state, LanternStore *store, const LanternSignedBlock *signed_block) { + if (!state || !store || !signed_block) { return -1; } const LanternBlock *block = &signed_block->message.block; @@ -2046,6 +2086,18 @@ int lantern_state_transition(LanternState *state, const LanternSignedBlock *sign if (block->slot <= state->slot) { STATE_FAIL("block slot %" PRIu64 " not ahead of state %" PRIu64, block->slot, state->slot); } + LanternSignedVote proposer_signed; + memset(&proposer_signed, 0, sizeof(proposer_signed)); + proposer_signed.data = signed_block->message.proposer_attestation; + proposer_signed.signature = signed_block->signatures.proposer_signature; + if (lantern_state_verify_block_signatures( + state, + block, + &signed_block->signatures, + &proposer_signed) + != 0) { + STATE_FAIL("block signatures invalid"); + } uint64_t slot_before = state->slot; double slots_metrics_start = lantern_time_now_seconds(); if (lantern_state_process_slots(state, block->slot) != 0) { @@ -2054,12 +2106,7 @@ int lantern_state_transition(LanternState *state, const LanternSignedBlock *sign double slots_duration = lantern_time_now_seconds() - slots_metrics_start; uint64_t slots_processed = block->slot >= slot_before ? (block->slot - slot_before) : 0; lean_metrics_record_state_transition_slots(slots_processed, slots_duration); - LanternSignedVote proposer_signed; - memset(&proposer_signed, 0, sizeof(proposer_signed)); - proposer_signed.data = signed_block->message.proposer_attestation; - proposer_signed.signature = signed_block->signatures.proposer_signature; - - if (lantern_state_process_block(state, block, &signed_block->signatures, &proposer_signed) != 0) { + if (lantern_state_process_block(state, store, block, NULL, &proposer_signed) != 0) { STATE_FAIL("process block failed"); } LanternRoot computed_state_root; @@ -2118,15 +2165,14 @@ int lantern_state_transition(LanternState *state, const LanternSignedBlock *sign "state", &(const struct lantern_log_metadata){.has_slot = true, .slot = block->slot}, "state root context state_slot=%" PRIu64 " header_slot=%" PRIu64 - " finalized_slot=%" PRIu64 " justified_offset=%" PRIu64 - " justified_bits=%zu hist_offset=%" PRIu64 " hist_len=%zu" + " finalized_slot=%" PRIu64 " justified_anchor=%" PRIu64 + " justified_bits=%zu hist_len=%zu" " just_roots=%zu just_votes=%zu", state->slot, state->latest_block_header.slot, state->latest_finalized.slot, - state->justified_slots_offset, + lantern_state_justified_slots_anchor(state), state->justified_slots.bit_length, - state->historical_roots_offset, state->historical_block_hashes.length, state->justification_roots.length, state->justification_validators.bit_length); @@ -2145,18 +2191,22 @@ int lantern_state_transition(LanternState *state, const LanternSignedBlock *sign STATE_FAIL("failed to hash state for slot %" PRIu64, block->slot); } - if (state->fork_choice) { - if (lantern_fork_choice_add_block( - state->fork_choice, + if (store->fork_choice) { + if (lantern_fork_choice_add_block_with_state( + store->fork_choice, block, &proposer_signed, &state->latest_justified, &state->latest_finalized, - NULL) + NULL, + state) != 0) { STATE_FAIL("fork choice add block failed for slot %" PRIu64, block->slot); } } + if (lantern_state_cache_proposer_attestation(state, store, &proposer_signed) != 0) { + STATE_FAIL("failed to cache proposer attestation for slot %" PRIu64, block->slot); + } state->slot = block->slot; lean_metrics_record_state_transition(lantern_time_now_seconds() - transition_metrics_start); @@ -2164,8 +2214,11 @@ int lantern_state_transition(LanternState *state, const LanternSignedBlock *sign return 0; } -int lantern_state_select_block_parent(LanternState *state, LanternRoot *out_parent_root) { - if (!state || !out_parent_root) { +int lantern_state_select_block_parent( + LanternState *state, + const LanternStore *store, + LanternRoot *out_parent_root) { + if (!state || !store || !out_parent_root) { return -1; } if (state->config.num_validators == 0) { @@ -2181,9 +2234,9 @@ int lantern_state_select_block_parent(LanternState *state, LanternRoot *out_pare return -1; } - if (state->fork_choice) { + if (store->fork_choice) { LanternRoot head_root; - if (lantern_fork_choice_current_head(state->fork_choice, &head_root) != 0) { + if (lantern_fork_choice_current_head(store->fork_choice, &head_root) != 0) { return -1; } if (memcmp(head_root.bytes, header_root.bytes, LANTERN_ROOT_SIZE) != 0) { @@ -2198,16 +2251,17 @@ int lantern_state_select_block_parent(LanternState *state, LanternRoot *out_pare int lantern_state_collect_attestations_for_block( const LanternState *state, + const LanternStore *store, uint64_t block_slot, uint64_t proposer_index, const LanternRoot *parent_root, const LanternSignedVote *proposer_attestation, LanternAttestations *out_attestations, LanternSignatureList *out_signatures) { - if (!state || !out_attestations || !out_signatures || !parent_root) { + if (!state || !store || !out_attestations || !out_signatures || !parent_root) { return -1; } - if (!state->validator_votes || state->validator_votes_len == 0) { + if (!store->validator_votes || store->validator_votes_len == 0) { return -1; } if (block_slot <= state->slot) { @@ -2225,6 +2279,10 @@ int lantern_state_collect_attestations_for_block( lantern_state_init(&slot_snapshot); LanternState scratch; lantern_state_init(&scratch); + LanternStore slot_store; + lantern_store_init(&slot_store); + LanternStore scratch_store; + lantern_store_init(&scratch_store); int rc = 0; bool fixed_point = false; const LanternSignedVote *proposer_ptr = proposer_attestation; @@ -2235,6 +2293,10 @@ int lantern_state_collect_attestations_for_block( rc = -1; goto cleanup; } + if (lantern_store_clone_validator_votes(store, &slot_store) != 0) { + rc = -1; + goto cleanup; + } if (lantern_state_process_slots(&slot_snapshot, block_slot) != 0) { rc = -1; goto cleanup; @@ -2242,7 +2304,7 @@ int lantern_state_collect_attestations_for_block( LanternCheckpoint checkpoint = slot_snapshot.latest_justified; size_t iteration = 0; - size_t iteration_guard = state->validator_votes_len == 0 ? 1 : state->validator_votes_len; + size_t iteration_guard = store->validator_votes_len == 0 ? 1 : store->validator_votes_len; if (iteration_guard < SIZE_MAX) { iteration_guard += 1u; } @@ -2251,7 +2313,14 @@ int lantern_state_collect_attestations_for_block( .slot = block_slot, }; while (true) { - if (collect_attestations_for_checkpoint(&slot_snapshot, &checkpoint, out_attestations, out_signatures) != 0) { + if (collect_attestations_for_checkpoint( + &slot_snapshot, + &slot_store, + store, + &checkpoint, + out_attestations, + out_signatures) + != 0) { rc = -1; goto cleanup; } @@ -2266,6 +2335,12 @@ int lantern_state_collect_attestations_for_block( rc = -1; goto cleanup; } + lantern_store_reset(&scratch_store); + lantern_store_init(&scratch_store); + if (lantern_store_clone_validator_votes(&slot_store, &scratch_store) != 0) { + rc = -1; + goto cleanup; + } LanternBlock candidate; memset(&candidate, 0, sizeof(candidate)); @@ -2276,13 +2351,19 @@ int lantern_state_collect_attestations_for_block( candidate.body.attestations.length = aggregated_view.length; candidate.body.attestations.capacity = aggregated_view.length; - if (lantern_state_process_block(&scratch, &candidate, NULL, proposer_ptr) != 0) { + if (lantern_state_process_block(&scratch, &scratch_store, &candidate, NULL, proposer_ptr) != 0) { rc = -1; goto cleanup; } LanternCheckpoint post_checkpoint = scratch.latest_justified; lantern_state_reset(&scratch); + lantern_store_reset(&slot_store); + lantern_store_init(&slot_store); + if (lantern_store_clone_validator_votes(&scratch_store, &slot_store) != 0) { + rc = -1; + goto cleanup; + } if (lantern_checkpoint_equal(&post_checkpoint, &checkpoint)) { fixed_point = true; break; @@ -2295,12 +2376,14 @@ int lantern_state_collect_attestations_for_block( &meta, "attestation collection failed to converge after %zu iterations (validators=%zu)", iteration, - state->validator_votes_len); + store->validator_votes_len); break; } } cleanup: + lantern_store_reset(&scratch_store); + lantern_store_reset(&slot_store); lantern_state_reset(&scratch); lantern_state_reset(&slot_snapshot); lantern_aggregated_attestations_reset(&aggregated_view); @@ -2316,19 +2399,26 @@ int lantern_state_collect_attestations_for_block( int lantern_state_preview_post_state_root( const LanternState *state, + const LanternStore *store, const LanternSignedBlock *block, LanternRoot *out_state_root) { - if (!state || !block || !out_state_root) { + if (!state || !store || !block || !out_state_root) { return -1; } if (block->message.block.slot <= 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) { return -1; } - scratch.fork_choice = NULL; + if (lantern_store_clone_validator_votes(store, &scratch_store) != 0) { + lantern_state_reset(&scratch); + return -1; + } int rc = 0; if (lantern_state_process_slots(&scratch, block->message.block.slot) != 0) { rc = -1; @@ -2338,7 +2428,13 @@ int lantern_state_preview_post_state_root( memset(&proposer_signed, 0, sizeof(proposer_signed)); proposer_signed.data = block->message.proposer_attestation; proposer_signed.signature = block->signatures.proposer_signature; - if (lantern_state_process_block(&scratch, &block->message.block, &block->signatures, &proposer_signed) != 0) { + if (lantern_state_process_block( + &scratch, + &scratch_store, + &block->message.block, + NULL, + &proposer_signed) + != 0) { rc = -1; goto cleanup; } @@ -2347,23 +2443,25 @@ int lantern_state_preview_post_state_root( } cleanup: + lantern_store_reset(&scratch_store); lantern_state_reset(&scratch); return rc; } int lantern_state_compute_vote_checkpoints( const LanternState *state, + const LanternStore *store, LanternCheckpoint *out_head, LanternCheckpoint *out_target, LanternCheckpoint *out_source) { - if (!state || !out_head || !out_target || !out_source) { + if (!state || !store || !out_head || !out_target || !out_source) { return -1; } - if (!state->fork_choice) { + if (!store->fork_choice) { return -1; } - const LanternForkChoice *store = state->fork_choice; + 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]; @@ -2371,15 +2469,15 @@ int lantern_state_compute_vote_checkpoints( 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(store, &head_root) != 0) { + if (lantern_fork_choice_current_head(fork_choice, &head_root) != 0) { return -1; } uint64_t head_slot = 0; - if (lantern_fork_choice_block_info(store, &head_root, &head_slot, NULL, NULL) != 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(store); - const LanternCheckpoint *store_finalized = lantern_fork_choice_latest_finalized(store); + 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; if (store_justified && !lantern_root_is_zero(&store_justified->root)) { @@ -2411,9 +2509,9 @@ int lantern_state_compute_vote_checkpoints( uint64_t safe_slot = head_slot; bool has_safe = false; - const LanternRoot *safe_ptr = lantern_fork_choice_safe_target(store); + const LanternRoot *safe_ptr = lantern_fork_choice_safe_target(fork_choice); if (safe_ptr) { - if (lantern_fork_choice_block_info(store, safe_ptr, &safe_slot, NULL, NULL) != 0) { + if (lantern_fork_choice_block_info(fork_choice, safe_ptr, &safe_slot, NULL, NULL) != 0) { return -1; } has_safe = true; @@ -2432,14 +2530,20 @@ int lantern_state_compute_vote_checkpoints( for (size_t i = 0; i < 3 && target_slot > safe_slot; ++i) { LanternRoot parent_root; bool has_parent = false; - if (lantern_fork_choice_block_info(store, &target_root, &target_slot, &parent_root, &has_parent) != 0) { + if (lantern_fork_choice_block_info( + fork_choice, + &target_root, + &target_slot, + &parent_root, + &has_parent) + != 0) { return -1; } if (!has_parent) { break; } uint64_t parent_slot = 0; - if (lantern_fork_choice_block_info(store, &parent_root, &parent_slot, NULL, NULL) != 0) { + if (lantern_fork_choice_block_info(fork_choice, &parent_root, &parent_slot, NULL, NULL) != 0) { return -1; } if (trace_finalization) { @@ -2470,7 +2574,13 @@ int lantern_state_compute_vote_checkpoints( while (!lantern_slot_is_justifiable(target_slot, finalized_checkpoint.slot)) { LanternRoot parent_root; bool has_parent = false; - if (lantern_fork_choice_block_info(store, &target_root, &target_slot, &parent_root, &has_parent) != 0) { + if (lantern_fork_choice_block_info( + fork_choice, + &target_root, + &target_slot, + &parent_root, + &has_parent) + != 0) { return -1; } if (!has_parent) { @@ -2478,7 +2588,7 @@ int lantern_state_compute_vote_checkpoints( break; } uint64_t parent_slot = 0; - if (lantern_fork_choice_block_info(store, &parent_root, &parent_slot, NULL, NULL) != 0) { + if (lantern_fork_choice_block_info(fork_choice, &parent_root, &parent_slot, NULL, NULL) != 0) { return -1; } if (parent_slot < finalized_checkpoint.slot) { diff --git a/src/consensus/store.c b/src/consensus/store.c new file mode 100644 index 0000000..36b55bf --- /dev/null +++ b/src/consensus/store.c @@ -0,0 +1,798 @@ +#include "lantern/consensus/store.h" + +#include +#include + +#include "lantern/consensus/fork_choice.h" + +static const size_t LANTERN_AGG_PROOF_CACHE_LIMIT = 4096u; +static const size_t LANTERN_GOSSIP_SIGNATURE_LIMIT = LANTERN_VALIDATOR_REGISTRY_LIMIT; + +static bool signature_is_zero(const LanternSignature *signature) { + if (!signature) { + return true; + } + for (size_t i = 0; i < LANTERN_SIGNATURE_SIZE; ++i) { + if (signature->bytes[i] != 0u) { + return false; + } + } + return true; +} + +static void lantern_vote_record_reset(struct lantern_vote_record *record) { + if (!record) { + return; + } + memset(record, 0, sizeof(*record)); +} + +static void fork_choice_votes_reset(struct lantern_fork_choice_vote_entry *entries, size_t count) { + if (!entries) { + return; + } + for (size_t i = 0; i < count; ++i) { + entries[i].has_checkpoint = false; + memset(&entries[i].checkpoint, 0, sizeof(entries[i].checkpoint)); + } +} + +static void sync_attached_fork_choice(LanternStore *store) { + if (!store || !store->fork_choice) { + return; + } + store->fork_choice->known_votes = store->known_votes; + store->fork_choice->new_votes = store->new_votes; + store->fork_choice->validator_count = store->fork_choice_vote_count; + store->fork_choice->new_aggregated_payloads = &store->new_aggregated_payloads; + store->fork_choice->known_aggregated_payloads = &store->known_aggregated_payloads; + store->fork_choice->attestation_data_by_root = &store->attestation_data_by_root; +} + +static void detach_attached_fork_choice(LanternStore *store) { + if (!store || !store->fork_choice) { + return; + } + store->fork_choice->known_votes = NULL; + store->fork_choice->new_votes = NULL; + store->fork_choice->validator_count = 0; + store->fork_choice->new_aggregated_payloads = NULL; + store->fork_choice->known_aggregated_payloads = NULL; + store->fork_choice->attestation_data_by_root = NULL; +} + +static bool signature_key_equals( + const LanternSignatureKey *lhs, + const LanternSignatureKey *rhs) { + if (!lhs || !rhs) { + return false; + } + return lhs->validator_index == rhs->validator_index + && memcmp(lhs->data_root.bytes, rhs->data_root.bytes, LANTERN_ROOT_SIZE) == 0; +} + +static bool root_equals(const LanternRoot *lhs, const LanternRoot *rhs) { + if (!lhs || !rhs) { + return false; + } + return memcmp(lhs->bytes, rhs->bytes, LANTERN_ROOT_SIZE) == 0; +} + +static bool root_in_set( + const LanternRoot *needle, + const LanternRoot *roots, + size_t root_count) { + if (!needle || (!roots && root_count > 0u)) { + return false; + } + for (size_t i = 0; i < root_count; ++i) { + if (root_equals(needle, &roots[i])) { + return true; + } + } + return false; +} + +static bool aggregated_payload_entry_equals( + const struct lantern_aggregated_payload_entry *entry, + const LanternRoot *data_root, + const LanternAggregatedSignatureProof *proof) { + if (!entry || !data_root || !proof) { + return false; + } + if (memcmp(entry->data_root.bytes, data_root->bytes, LANTERN_ROOT_SIZE) != 0) { + return false; + } + if (entry->proof.participants.bit_length != proof->participants.bit_length) { + return false; + } + size_t bits = proof->participants.bit_length; + size_t bytes = (bits + 7u) / 8u; + if (bytes > 0) { + if (!entry->proof.participants.bytes || !proof->participants.bytes) { + return false; + } + if (memcmp(entry->proof.participants.bytes, proof->participants.bytes, bytes) != 0) { + return false; + } + } + if (entry->proof.proof_data.length != proof->proof_data.length) { + return false; + } + if (proof->proof_data.length > 0) { + if (!entry->proof.proof_data.data || !proof->proof_data.data) { + return false; + } + if (memcmp(entry->proof.proof_data.data, proof->proof_data.data, proof->proof_data.length) != 0) { + return false; + } + } + return true; +} + +static void gossip_signature_map_init(struct lantern_gossip_signature_map *map) { + if (!map) { + return; + } + map->entries = NULL; + map->length = 0; + map->capacity = 0; +} + +static void gossip_signature_map_reset(struct lantern_gossip_signature_map *map) { + if (!map) { + return; + } + free(map->entries); + map->entries = NULL; + map->length = 0; + map->capacity = 0; +} + +static void gossip_signature_map_remove_index( + struct lantern_gossip_signature_map *map, + size_t index) { + if (!map || !map->entries || index >= map->length) { + return; + } + if (index + 1u < map->length) { + memmove( + &map->entries[index], + &map->entries[index + 1u], + (map->length - index - 1u) * sizeof(*map->entries)); + } + map->length -= 1u; +} + +static int gossip_signature_map_set( + struct lantern_gossip_signature_map *map, + const LanternSignatureKey *key, + const LanternSignature *signature, + uint64_t target_slot) { + if (!map || !key || !signature || signature_is_zero(signature)) { + return -1; + } + for (size_t i = 0; i < map->length; ++i) { + if (!signature_key_equals(&map->entries[i].key, key)) { + continue; + } + map->entries[i].signature = *signature; + map->entries[i].target_slot = target_slot; + return 0; + } + if (map->length >= LANTERN_GOSSIP_SIGNATURE_LIMIT) { + gossip_signature_map_remove_index(map, 0u); + } + if (map->length >= map->capacity) { + size_t desired = map->capacity == 0 ? 8u : map->capacity * 2u; + if (desired > LANTERN_GOSSIP_SIGNATURE_LIMIT) { + desired = LANTERN_GOSSIP_SIGNATURE_LIMIT; + } + if (desired <= map->capacity) { + return -1; + } + struct lantern_gossip_signature_entry *entries = + realloc(map->entries, desired * sizeof(*entries)); + if (!entries) { + return -1; + } + map->entries = entries; + map->capacity = desired; + } + map->entries[map->length].key = *key; + map->entries[map->length].signature = *signature; + map->entries[map->length].target_slot = target_slot; + map->length += 1u; + return 0; +} + +static int gossip_signature_map_get( + const struct lantern_gossip_signature_map *map, + const LanternSignatureKey *key, + LanternSignature *out_signature) { + if (!map || !key || !out_signature) { + return -1; + } + for (size_t i = 0; i < map->length; ++i) { + if (!signature_key_equals(&map->entries[i].key, key)) { + continue; + } + *out_signature = map->entries[i].signature; + return 0; + } + return -1; +} + +static void aggregated_payload_pool_init(struct lantern_aggregated_payload_pool *cache) { + if (!cache) { + return; + } + cache->entries = NULL; + cache->length = 0; + cache->capacity = 0; +} + +static void aggregated_payload_pool_reset(struct lantern_aggregated_payload_pool *cache) { + if (!cache) { + return; + } + if (cache->entries) { + for (size_t i = 0; i < cache->length; ++i) { + lantern_aggregated_signature_proof_reset(&cache->entries[i].proof); + } + } + free(cache->entries); + cache->entries = NULL; + cache->length = 0; + cache->capacity = 0; +} + +static void aggregated_payload_pool_remove_index( + struct lantern_aggregated_payload_pool *cache, + size_t index) { + if (!cache || !cache->entries || index >= cache->length) { + return; + } + lantern_aggregated_signature_proof_reset(&cache->entries[index].proof); + if (index + 1u < cache->length) { + memmove( + &cache->entries[index], + &cache->entries[index + 1u], + (cache->length - index - 1u) * sizeof(*cache->entries)); + } + cache->length -= 1u; +} + +static int aggregated_payload_pool_add( + struct lantern_aggregated_payload_pool *cache, + const LanternRoot *data_root, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot) { + if (!cache || !data_root || !proof) { + return -1; + } + if (proof->participants.bit_length == 0 || proof->proof_data.length == 0) { + return -1; + } + for (size_t i = 0; i < cache->length; ++i) { + if (!aggregated_payload_entry_equals(&cache->entries[i], data_root, proof)) { + continue; + } + cache->entries[i].target_slot = target_slot; + return 0; + } + if (cache->length >= LANTERN_AGG_PROOF_CACHE_LIMIT) { + aggregated_payload_pool_remove_index(cache, 0u); + } + if (cache->length >= cache->capacity) { + size_t desired = cache->capacity == 0 ? 8u : cache->capacity * 2u; + if (desired > LANTERN_AGG_PROOF_CACHE_LIMIT) { + desired = LANTERN_AGG_PROOF_CACHE_LIMIT; + } + if (desired <= cache->capacity) { + return -1; + } + struct lantern_aggregated_payload_entry *entries = + realloc(cache->entries, desired * sizeof(*entries)); + if (!entries) { + return -1; + } + cache->entries = entries; + cache->capacity = desired; + } + struct lantern_aggregated_payload_entry *entry = &cache->entries[cache->length]; + entry->data_root = *data_root; + lantern_aggregated_signature_proof_init(&entry->proof); + if (lantern_aggregated_signature_proof_copy(&entry->proof, proof) != 0) { + lantern_aggregated_signature_proof_reset(&entry->proof); + return -1; + } + entry->target_slot = target_slot; + cache->length += 1u; + return 0; +} + +static void attestation_data_by_root_init(struct lantern_attestation_data_by_root *cache) { + if (!cache) { + return; + } + cache->entries = NULL; + cache->length = 0; + cache->capacity = 0; +} + +static void attestation_data_by_root_reset(struct lantern_attestation_data_by_root *cache) { + if (!cache) { + return; + } + free(cache->entries); + cache->entries = NULL; + cache->length = 0; + cache->capacity = 0; +} + +static void attestation_data_by_root_remove_index( + struct lantern_attestation_data_by_root *cache, + size_t index) { + if (!cache || !cache->entries || index >= cache->length) { + return; + } + if (index + 1u < cache->length) { + memmove( + &cache->entries[index], + &cache->entries[index + 1u], + (cache->length - index - 1u) * sizeof(*cache->entries)); + } + cache->length -= 1u; +} + +static int attestation_data_by_root_add( + struct lantern_attestation_data_by_root *cache, + const LanternRoot *data_root, + const LanternAttestationData *data, + uint64_t target_slot) { + if (!cache || !data_root || !data) { + return -1; + } + for (size_t i = 0; i < cache->length; ++i) { + if (memcmp(cache->entries[i].data_root.bytes, data_root->bytes, LANTERN_ROOT_SIZE) == 0) { + cache->entries[i].data = *data; + cache->entries[i].target_slot = target_slot; + return 0; + } + } + if (cache->length >= cache->capacity) { + size_t desired = cache->capacity == 0 ? 8u : cache->capacity * 2u; + if (desired <= cache->capacity) { + return -1; + } + struct lantern_attestation_data_by_root_entry *entries = + realloc(cache->entries, desired * sizeof(*entries)); + if (!entries) { + return -1; + } + cache->entries = entries; + cache->capacity = desired; + } + cache->entries[cache->length].data_root = *data_root; + cache->entries[cache->length].data = *data; + cache->entries[cache->length].target_slot = target_slot; + cache->length += 1u; + return 0; +} + +void lantern_store_init(LanternStore *store) { + if (!store) { + return; + } + memset(store, 0, sizeof(*store)); + gossip_signature_map_init(&store->gossip_signatures); + aggregated_payload_pool_init(&store->new_aggregated_payloads); + aggregated_payload_pool_init(&store->known_aggregated_payloads); + attestation_data_by_root_init(&store->attestation_data_by_root); +} + +void lantern_store_reset(LanternStore *store) { + if (!store) { + return; + } + detach_attached_fork_choice(store); + free(store->validator_votes); + store->validator_votes = NULL; + store->validator_votes_len = 0; + free(store->known_votes); + store->known_votes = NULL; + free(store->new_votes); + store->new_votes = NULL; + store->fork_choice_vote_count = 0; + gossip_signature_map_reset(&store->gossip_signatures); + aggregated_payload_pool_reset(&store->new_aggregated_payloads); + aggregated_payload_pool_reset(&store->known_aggregated_payloads); + attestation_data_by_root_reset(&store->attestation_data_by_root); + store->fork_choice = NULL; +} + +void lantern_store_attach_fork_choice(LanternStore *store, struct lantern_fork_choice *fork_choice) { + if (!store) { + return; + } + detach_attached_fork_choice(store); + store->fork_choice = fork_choice; + sync_attached_fork_choice(store); +} + +int lantern_store_prepare_validator_votes(LanternStore *store, uint64_t validator_count) { + if (!store || validator_count == 0) { + return -1; + } + if (validator_count > (uint64_t)LANTERN_VALIDATOR_REGISTRY_LIMIT || validator_count > SIZE_MAX) { + return -1; + } + size_t count = (size_t)validator_count; + if (store->validator_votes && store->validator_votes_len != count) { + free(store->validator_votes); + store->validator_votes = NULL; + store->validator_votes_len = 0; + } + if (!store->validator_votes) { + struct lantern_vote_record *records = calloc(count, sizeof(*records)); + if (!records) { + return -1; + } + store->validator_votes = records; + store->validator_votes_len = count; + } else { + for (size_t i = 0; i < count; ++i) { + lantern_vote_record_reset(&store->validator_votes[i]); + } + } + return 0; +} + +int lantern_store_clone_validator_votes(const LanternStore *source, LanternStore *dest) { + if (!dest) { + return -1; + } + if (!source || !source->validator_votes || source->validator_votes_len == 0) { + return 0; + } + if (lantern_store_prepare_validator_votes(dest, (uint64_t)source->validator_votes_len) != 0) { + return -1; + } + memcpy( + dest->validator_votes, + source->validator_votes, + source->validator_votes_len * sizeof(*source->validator_votes)); + return 0; +} + +size_t lantern_store_validator_capacity(const LanternStore *store) { + if (!store || !store->validator_votes) { + return 0; + } + return store->validator_votes_len; +} + +bool lantern_store_validator_has_vote(const LanternStore *store, size_t index) { + if (!store || !store->validator_votes || index >= store->validator_votes_len) { + return false; + } + return store->validator_votes[index].has_vote; +} + +int lantern_store_get_signed_validator_vote( + const LanternStore *store, + size_t index, + LanternSignedVote *out_vote) { + if (!store || !store->validator_votes || index >= store->validator_votes_len || !out_vote) { + return -1; + } + const struct lantern_vote_record *record = &store->validator_votes[index]; + if (!record->has_vote) { + return -1; + } + memset(out_vote, 0, sizeof(*out_vote)); + out_vote->data = record->vote; + out_vote->data.validator_id = (uint64_t)index; + if (record->has_signature) { + out_vote->signature = record->signature; + } + return 0; +} + +int lantern_store_get_validator_vote(const LanternStore *store, size_t index, LanternVote *out_vote) { + if (!out_vote) { + return -1; + } + LanternSignedVote signed_vote; + if (lantern_store_get_signed_validator_vote(store, index, &signed_vote) != 0) { + return -1; + } + *out_vote = signed_vote.data; + return 0; +} + +int lantern_store_set_signed_validator_vote( + LanternStore *store, + size_t index, + const LanternSignedVote *vote) { + if (!store || !store->validator_votes || index >= store->validator_votes_len || !vote) { + return -1; + } + struct lantern_vote_record *record = &store->validator_votes[index]; + LanternVote previous_vote = record->vote; + LanternSignature previous_signature = record->signature; + bool previous_has_signature = record->has_signature; + record->vote = vote->data; + record->vote.validator_id = (uint64_t)index; + record->has_vote = true; + if (!signature_is_zero(&vote->signature)) { + record->signature = vote->signature; + record->has_signature = true; + } else if (previous_has_signature + && memcmp(&previous_vote, &record->vote, sizeof(previous_vote)) == 0) { + record->signature = previous_signature; + record->has_signature = true; + } else { + memset(&record->signature, 0, sizeof(record->signature)); + record->has_signature = false; + } + return 0; +} + +int lantern_store_set_validator_vote(LanternStore *store, size_t index, const LanternVote *vote) { + if (!vote) { + return -1; + } + LanternSignedVote signed_vote; + memset(&signed_vote, 0, sizeof(signed_vote)); + signed_vote.data = *vote; + return lantern_store_set_signed_validator_vote(store, index, &signed_vote); +} + +void lantern_store_clear_validator_vote(LanternStore *store, size_t index) { + if (!store || !store->validator_votes || index >= store->validator_votes_len) { + return; + } + lantern_vote_record_reset(&store->validator_votes[index]); +} + +int lantern_store_prepare_fork_choice_votes(LanternStore *store, uint64_t validator_count) { + if (!store || validator_count == 0) { + return -1; + } + if (validator_count > (uint64_t)LANTERN_VALIDATOR_REGISTRY_LIMIT || validator_count > SIZE_MAX) { + return -1; + } + size_t count = (size_t)validator_count; + if (store->known_votes && store->fork_choice_vote_count != count) { + free(store->known_votes); + store->known_votes = NULL; + } + if (store->new_votes && store->fork_choice_vote_count != count) { + free(store->new_votes); + store->new_votes = NULL; + } + if (!store->known_votes) { + store->known_votes = calloc(count, sizeof(*store->known_votes)); + if (!store->known_votes) { + return -1; + } + } + if (!store->new_votes) { + store->new_votes = calloc(count, sizeof(*store->new_votes)); + if (!store->new_votes) { + free(store->known_votes); + store->known_votes = NULL; + return -1; + } + } + fork_choice_votes_reset(store->known_votes, count); + fork_choice_votes_reset(store->new_votes, count); + store->fork_choice_vote_count = count; + sync_attached_fork_choice(store); + return 0; +} + +int lantern_store_set_gossip_signature( + LanternStore *store, + const LanternSignatureKey *key, + 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) { + return -1; + } + if (data) { + (void)attestation_data_by_root_add( + &store->attestation_data_by_root, + &key->data_root, + data, + target_slot); + } + return 0; +} + +int lantern_store_get_gossip_signature( + const LanternStore *store, + const LanternSignatureKey *key, + LanternSignature *out_signature) { + if (!store) { + return -1; + } + return gossip_signature_map_get(&store->gossip_signatures, key, out_signature); +} + +static int lantern_store_add_aggregated_payload( + LanternStore *store, + struct lantern_aggregated_payload_pool *pool, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot) { + if (!store || !pool || !data_root || !proof) { + return -1; + } + if (aggregated_payload_pool_add(pool, data_root, proof, target_slot) != 0) { + return -1; + } + if (data) { + (void)attestation_data_by_root_add(&store->attestation_data_by_root, data_root, data, target_slot); + } + return 0; +} + +int lantern_store_add_new_aggregated_payload( + LanternStore *store, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot) { + if (!store) { + return -1; + } + return lantern_store_add_aggregated_payload( + store, + &store->new_aggregated_payloads, + data_root, + data, + proof, + target_slot); +} + +int lantern_store_add_known_aggregated_payload( + LanternStore *store, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot) { + if (!store) { + return -1; + } + return lantern_store_add_aggregated_payload( + store, + &store->known_aggregated_payloads, + data_root, + data, + proof, + target_slot); +} + +size_t lantern_store_promote_new_aggregated_payloads(LanternStore *store) { + if (!store) { + return 0u; + } + size_t moved = 0u; + size_t index = 0u; + while (index < store->new_aggregated_payloads.length) { + const struct lantern_aggregated_payload_entry *entry = + &store->new_aggregated_payloads.entries[index]; + int add_rc = lantern_store_add_known_aggregated_payload( + store, + &entry->data_root, + NULL, + &entry->proof, + entry->target_slot); + if (add_rc != 0) { + break; + } + aggregated_payload_pool_remove_index(&store->new_aggregated_payloads, index); + moved += 1u; + } + return moved; +} + +size_t lantern_store_prune_finalized_attestation_material( + LanternStore *store, + uint64_t finalized_slot) { + if (!store) { + return 0u; + } + + struct lantern_attestation_data_by_root *data_cache = &store->attestation_data_by_root; + size_t stale_root_count = 0u; + for (size_t i = 0; i < data_cache->length; ++i) { + if (data_cache->entries[i].target_slot <= finalized_slot) { + stale_root_count += 1u; + } + } + if (stale_root_count == 0u) { + return 0u; + } + + LanternRoot *stale_roots = malloc(stale_root_count * sizeof(*stale_roots)); + if (!stale_roots) { + return 0u; + } + + size_t stale_root_index = 0u; + size_t data_index = 0u; + while (data_index < data_cache->length) { + if (data_cache->entries[data_index].target_slot <= finalized_slot) { + stale_roots[stale_root_index] = data_cache->entries[data_index].data_root; + stale_root_index += 1u; + attestation_data_by_root_remove_index(data_cache, data_index); + continue; + } + data_index += 1u; + } + + size_t signature_index = 0u; + while (signature_index < store->gossip_signatures.length) { + if (root_in_set( + &store->gossip_signatures.entries[signature_index].key.data_root, + stale_roots, + stale_root_count)) { + gossip_signature_map_remove_index(&store->gossip_signatures, signature_index); + continue; + } + signature_index += 1u; + } + + size_t proof_index = 0u; + while (proof_index < store->new_aggregated_payloads.length) { + if (root_in_set( + &store->new_aggregated_payloads.entries[proof_index].data_root, + stale_roots, + stale_root_count)) { + aggregated_payload_pool_remove_index(&store->new_aggregated_payloads, proof_index); + continue; + } + proof_index += 1u; + } + + proof_index = 0u; + while (proof_index < store->known_aggregated_payloads.length) { + if (root_in_set( + &store->known_aggregated_payloads.entries[proof_index].data_root, + stale_roots, + stale_root_count)) { + aggregated_payload_pool_remove_index(&store->known_aggregated_payloads, proof_index); + continue; + } + proof_index += 1u; + } + + free(stale_roots); + return stale_root_count; +} + +int lantern_store_get_attestation_data( + const LanternStore *store, + const LanternRoot *data_root, + LanternAttestationData *out_data) { + if (!store || !data_root || !out_data) { + return -1; + } + const struct lantern_attestation_data_by_root *cache = &store->attestation_data_by_root; + for (size_t i = 0; i < cache->length; ++i) { + if (memcmp(cache->entries[i].data_root.bytes, data_root->bytes, LANTERN_ROOT_SIZE) == 0) { + *out_data = cache->entries[i].data; + return 0; + } + } + return -1; +} diff --git a/src/core/client.c b/src/core/client.c index 565ed34..cb53e17 100644 --- a/src/core/client.c +++ b/src/core/client.c @@ -72,7 +72,6 @@ static const size_t NODE_PRIVATE_KEY_SIZE = 32u; static const size_t BOOTNODE_LINE_MAX_LEN = 2048u; -static const size_t LANTERN_AGG_PROOF_CACHE_LIMIT = 4096u; static const size_t CHECKPOINT_SYNC_MAX_RESPONSE_BYTES = 512u + (LANTERN_HISTORICAL_ROOTS_LIMIT * 32u) @@ -82,158 +81,198 @@ static const size_t CHECKPOINT_SYNC_MAX_RESPONSE_BYTES = + (LANTERN_JUSTIFICATION_VALIDATORS_LIMIT / 8u); static const size_t LANTERN_ATTESTATION_COMMITTEE_COUNT = 1u; -static void agg_proof_cache_init(struct lantern_agg_proof_cache *cache) { - if (!cache) { +static void sync_aggregated_payload_pools_after_time_advance( + struct lantern_client *client, + uint64_t previous_intervals, + bool has_proposal) { + if (!client || !client->has_fork_choice) { return; } - cache->entries = NULL; - cache->length = 0; - cache->capacity = 0; -} - -static void agg_proof_cache_reset(struct lantern_agg_proof_cache *cache) { - if (!cache) { + if (client->fork_choice.intervals_per_slot == 0 + || client->fork_choice.time_intervals <= previous_intervals) { return; } - if (cache->entries) { - for (size_t i = 0; i < cache->length; ++i) { - lantern_aggregated_signature_proof_reset(&cache->entries[i].proof); + for (uint64_t step = previous_intervals + 1u; + step <= client->fork_choice.time_intervals; + ++step) { + uint64_t interval_index = step % client->fork_choice.intervals_per_slot; + bool step_has_proposal = has_proposal && (step == client->fork_choice.time_intervals); + if (interval_index == 4u || (interval_index == 0u && step_has_proposal)) { + (void)lantern_store_promote_new_aggregated_payloads(&client->store); } } - free(cache->entries); - cache->entries = NULL; - cache->length = 0; - cache->capacity = 0; } -static bool agg_proof_cache_entry_equals( - const struct lantern_agg_proof_cache_entry *entry, - const LanternRoot *data_root, - const LanternAggregatedSignatureProof *proof) { - if (!entry || !data_root || !proof) { - return false; +static int fork_choice_interval_boundary_milliseconds( + const LanternForkChoice *store, + uint64_t target_interval, + uint64_t *out_now_milliseconds) { + if (!store || !out_now_milliseconds || store->milliseconds_per_interval == 0) { + return -1; } - if (memcmp(entry->data_root.bytes, data_root->bytes, LANTERN_ROOT_SIZE) != 0) { - return false; + if (store->config.genesis_time > UINT64_MAX / 1000u) { + return -1; } - if (entry->proof.participants.bit_length != proof->participants.bit_length) { - return false; + uint64_t genesis_milliseconds = store->config.genesis_time * 1000u; + if (target_interval > (UINT64_MAX - genesis_milliseconds) / store->milliseconds_per_interval) { + return -1; } - size_t bits = proof->participants.bit_length; - size_t bytes = (bits + 7u) / 8u; - if (bytes > 0) { - if (!entry->proof.participants.bytes || !proof->participants.bytes) { - return false; - } - if (memcmp(entry->proof.participants.bytes, proof->participants.bytes, bytes) != 0) { - return false; - } + *out_now_milliseconds = + genesis_milliseconds + (target_interval * store->milliseconds_per_interval); + return 0; +} + +static int fork_choice_target_interval( + const LanternForkChoice *store, + uint64_t now_milliseconds, + uint64_t *out_target_interval) { + if (!store || !out_target_interval || store->milliseconds_per_interval == 0) { + return -1; } - if (entry->proof.proof_data.length != proof->proof_data.length) { - return false; + if (store->config.genesis_time > UINT64_MAX / 1000u) { + return -1; } - if (proof->proof_data.length > 0) { - if (!entry->proof.proof_data.data || !proof->proof_data.data) { - return false; - } - if (memcmp(entry->proof.proof_data.data, proof->proof_data.data, proof->proof_data.length) != 0) { - return false; - } + uint64_t genesis_milliseconds = store->config.genesis_time * 1000u; + if (now_milliseconds < genesis_milliseconds) { + return 1; } - return true; + *out_target_interval = (now_milliseconds - genesis_milliseconds) / store->milliseconds_per_interval; + return 0; } -static void agg_proof_cache_remove_index(struct lantern_agg_proof_cache *cache, size_t index) { - if (!cache || !cache->entries || index >= cache->length) { - return; - } - lantern_aggregated_signature_proof_reset(&cache->entries[index].proof); - if (index + 1u < cache->length) { - memmove( - &cache->entries[index], - &cache->entries[index + 1u], - (cache->length - index - 1u) * sizeof(*cache->entries)); +int lantern_client_set_gossip_signature( + struct lantern_client *client, + const LanternSignatureKey *key, + const LanternAttestationData *data, + const LanternSignature *signature, + uint64_t target_slot) { + if (!client) { + return -1; } - cache->length -= 1u; + return lantern_store_set_gossip_signature(&client->store, key, data, signature, target_slot); } -static void agg_proof_cache_evict_oldest(struct lantern_agg_proof_cache *cache) { - agg_proof_cache_remove_index(cache, 0u); +int lantern_client_add_new_aggregated_payload( + struct lantern_client *client, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot) { + if (!client) { + return -1; + } + return lantern_store_add_new_aggregated_payload(&client->store, data_root, data, proof, target_slot); } -int lantern_client_agg_proof_cache_add( +int lantern_client_add_known_aggregated_payload( struct lantern_client *client, const LanternRoot *data_root, + const LanternAttestationData *data, const LanternAggregatedSignatureProof *proof, uint64_t target_slot) { - if (!client || !data_root || !proof) { + if (!client) { return -1; } - if (proof->participants.bit_length == 0 || proof->proof_data.length == 0) { + return lantern_store_add_known_aggregated_payload(&client->store, data_root, data, proof, target_slot); +} + +size_t lantern_client_promote_new_aggregated_payloads( + struct lantern_client *client) { + if (!client) { + return 0u; + } + return lantern_store_promote_new_aggregated_payloads(&client->store); +} + +size_t lantern_client_prune_finalized_attestation_material( + struct lantern_client *client, + uint64_t finalized_slot) { + if (!client) { + return 0u; + } + return lantern_store_prune_finalized_attestation_material(&client->store, finalized_slot); +} + +int lantern_client_tick_fork_choice_interval_locked( + struct lantern_client *client, + bool has_proposal) { + if (!client || !client->has_fork_choice) { return -1; } - struct lantern_agg_proof_cache *cache = &client->agg_proof_cache; - for (size_t i = 0; i < cache->length; ++i) { - if (agg_proof_cache_entry_equals(&cache->entries[i], data_root, proof)) { - cache->entries[i].target_slot = target_slot; - return 0; - } + + uint64_t previous_intervals = client->fork_choice.time_intervals; + uint64_t next_interval = previous_intervals + 1u; + if (next_interval < previous_intervals) { + return -1; } - if (cache->length >= LANTERN_AGG_PROOF_CACHE_LIMIT) { - agg_proof_cache_evict_oldest(cache); + uint64_t now_milliseconds = 0; + if (fork_choice_interval_boundary_milliseconds( + &client->fork_choice, + next_interval, + &now_milliseconds) + != 0) { + return -1; } - if (cache->length >= cache->capacity) { - size_t desired = cache->capacity == 0 ? 8u : cache->capacity * 2u; - if (desired > LANTERN_AGG_PROOF_CACHE_LIMIT) { - desired = LANTERN_AGG_PROOF_CACHE_LIMIT; - } - if (desired <= cache->capacity) { - return -1; - } - struct lantern_agg_proof_cache_entry *entries = - realloc(cache->entries, desired * sizeof(*entries)); - if (!entries) { - return -1; - } - cache->entries = entries; - cache->capacity = desired; + int rc = lantern_fork_choice_advance_time(&client->fork_choice, now_milliseconds, has_proposal); + if (rc != 0) { + return rc; } + if (client->fork_choice.time_intervals != next_interval) { + return -1; + } + + sync_aggregated_payload_pools_after_time_advance(client, previous_intervals, has_proposal); + return 0; +} - struct lantern_agg_proof_cache_entry *entry = &cache->entries[cache->length]; - entry->data_root = *data_root; - lantern_aggregated_signature_proof_init(&entry->proof); - if (lantern_aggregated_signature_proof_copy(&entry->proof, proof) != 0) { - lantern_aggregated_signature_proof_reset(&entry->proof); +int lantern_client_skip_fork_choice_intervals_locked( + struct lantern_client *client, + uint64_t target_interval) { + if (!client || !client->has_fork_choice) { + return -1; + } + if (target_interval < client->fork_choice.time_intervals) { return -1; } - entry->target_slot = target_slot; - cache->length += 1u; + client->fork_choice.time_intervals = target_interval; return 0; } -size_t lantern_client_agg_proof_cache_prune_finalized( +int lantern_client_advance_fork_choice_time_locked( struct lantern_client *client, - uint64_t finalized_slot) { - if (!client) { - return 0u; + uint64_t now_milliseconds, + bool has_proposal) { + if (!client || !client->has_fork_choice) { + return -1; } - struct lantern_agg_proof_cache *cache = &client->agg_proof_cache; - if (!cache->entries || cache->length == 0) { - return 0u; + + uint64_t previous_intervals = client->fork_choice.time_intervals; + uint64_t target_interval = 0u; + int target_rc = fork_choice_target_interval(&client->fork_choice, now_milliseconds, &target_interval); + if (target_rc < 0) { + return -1; } - size_t removed = 0u; - size_t index = 0u; - while (index < cache->length) { - if (cache->entries[index].target_slot <= finalized_slot) { - agg_proof_cache_remove_index(cache, index); - removed += 1u; - continue; + if (target_rc == 0 + && client->fork_choice.intervals_per_slot > 0 + && target_interval > previous_intervals + && (target_interval - previous_intervals) > client->fork_choice.intervals_per_slot) + { + uint64_t skip_to_interval = + target_interval - (uint64_t)client->fork_choice.intervals_per_slot; + if (lantern_client_skip_fork_choice_intervals_locked(client, skip_to_interval) != 0) { + return -1; } - index += 1u; + previous_intervals = client->fork_choice.time_intervals; + } + + int rc = lantern_fork_choice_advance_time(&client->fork_choice, now_milliseconds, has_proposal); + if (rc != 0) { + return rc; } - return removed; + sync_aggregated_payload_pools_after_time_advance(client, previous_intervals, has_proposal); + return 0; } /* ============================================================================ @@ -596,11 +635,14 @@ static void client_reset_base(struct lantern_client *client) lantern_http_server_init(&client->http_server); client->http_running = false; lantern_state_init(&client->state); - agg_proof_cache_init(&client->agg_proof_cache); + lantern_store_init(&client->store); lean_metrics_reset(); client->state_lock_initialized = false; lantern_fork_choice_init(&client->fork_choice); + lantern_store_attach_fork_choice(&client->store, &client->fork_choice); client->has_fork_choice = false; + client->timing_thread_started = false; + client->timing_stop_flag = 1; client->dialer_thread_started = false; client->dialer_stop_flag = 1; client->ping_thread_started = false; @@ -1133,9 +1175,13 @@ static void client_log_genesis_anchors( */ static lantern_client_error client_finalize_genesis_state(struct lantern_client *client) { - if (lantern_state_prepare_validator_votes( - &client->state, + if (lantern_store_prepare_validator_votes( + &client->store, client->state.config.num_validators) + != 0 + || lantern_store_prepare_fork_choice_votes( + &client->store, + client->state.config.num_validators) != 0) { lantern_log_error( @@ -2198,9 +2244,13 @@ static lantern_client_error client_load_state_from_checkpoint( goto cleanup; } - if (lantern_state_prepare_validator_votes( - &decoded, + if (lantern_store_prepare_validator_votes( + &client->store, decoded.config.num_validators) + != 0 + || lantern_store_prepare_fork_choice_votes( + &client->store, + decoded.config.num_validators) != 0) { lantern_log_error( @@ -2375,7 +2425,7 @@ static lantern_client_error client_load_or_build_state( if (client->has_state) { - int votes_rc = lantern_storage_load_votes(client->data_dir, &client->state); + int votes_rc = lantern_storage_load_votes(client->data_dir, &client->state, &client->store); if (votes_rc < 0) { lantern_log_error( @@ -2403,7 +2453,7 @@ static lantern_client_error client_load_or_build_state( &(const struct lantern_log_metadata){.validator = client->node_id}, "failed to persist initial state snapshot"); } - if (lantern_storage_save_votes(client->data_dir, &client->state) != 0) + if (lantern_storage_save_votes(client->data_dir, &client->state, &client->store) != 0) { lantern_log_warn( "storage", @@ -2795,6 +2845,14 @@ static void client_start_background_services(struct lantern_client *client) "failed to start ping service thread"); } + if (start_timing_service(client) != 0) + { + lantern_log_warn( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "fork-choice timing inactive"); + } + if (start_validator_service(client) != 0) { lantern_log_warn( @@ -2817,6 +2875,7 @@ static void client_start_background_services(struct lantern_client *client) */ static void shutdown_validator_and_keys(struct lantern_client *client) { + stop_timing_service(client); stop_validator_service(client); stop_ping_service(client); stop_peer_dialer(client); @@ -3142,7 +3201,7 @@ static void shutdown_state_and_runtime(struct lantern_client *client) { lantern_state_reset(&client->state); } - agg_proof_cache_reset(&client->agg_proof_cache); + lantern_store_reset(&client->store); if (client->state_lock_initialized) { pthread_mutex_destroy(&client->state_lock); diff --git a/src/core/client_http.c b/src/core/client_http.c index bc200aa..a1c9bee 100644 --- a/src/core/client_http.c +++ b/src/core/client_http.c @@ -356,8 +356,9 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap memset(&fork_head_root, 0, sizeof(fork_head_root)); uint64_t fork_head_slot = 0; uint64_t safe_target_slot = 0; - size_t new_vote_count = 0; - size_t known_vote_count = 0; + size_t gossip_signature_count = 0; + size_t new_aggregated_payload_count = 0; + size_t known_aggregated_payload_count = 0; if (client->has_fork_choice) { if (lantern_fork_choice_current_head(&client->fork_choice, &fork_head_root) == 0) @@ -392,9 +393,10 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap } } - new_vote_count = lantern_fork_choice_new_votes_count(&client->fork_choice); - known_vote_count = lantern_fork_choice_known_votes_count(&client->fork_choice); } + gossip_signature_count = client->store.gossip_signatures.length; + new_aggregated_payload_count = client->store.new_aggregated_payloads.length; + known_aggregated_payload_count = client->store.known_aggregated_payloads.length; uint64_t state_head_slot = 0; uint64_t current_slot = 0; @@ -423,9 +425,9 @@ int metrics_snapshot_cb(void *context, struct lantern_metrics_snapshot *out_snap out_snapshot->lean_latest_justified_slot = state_justified.slot; out_snapshot->lean_latest_finalized_slot = state_finalized.slot; out_snapshot->lean_validators_count = client->local_validator_count; - out_snapshot->lean_gossip_signatures_count = (uint64_t)(new_vote_count + known_vote_count); - out_snapshot->lean_latest_new_aggregated_payloads_count = (uint64_t)new_vote_count; - out_snapshot->lean_latest_known_aggregated_payloads_count = (uint64_t)known_vote_count; + out_snapshot->lean_gossip_signatures_count = (uint64_t)gossip_signature_count; + out_snapshot->lean_latest_new_aggregated_payloads_count = (uint64_t)new_aggregated_payload_count; + out_snapshot->lean_latest_known_aggregated_payloads_count = (uint64_t)known_aggregated_payload_count; out_snapshot->lean_is_aggregator = (client->assigned_validators && client->assigned_validators->enr.is_aggregator) ? 1u : 0u; out_snapshot->lean_committee_attestation_subnet = (uint64_t)client->gossip.attestation_subnet_id; diff --git a/src/core/client_internal.h b/src/core/client_internal.h index 4eedbcc..695e67c 100644 --- a/src/core/client_internal.h +++ b/src/core/client_internal.h @@ -187,23 +187,57 @@ void string_list_remove(struct lantern_string_list *list, const char *value); const char *connection_reason_text(int reason); /** - * Cache an aggregated signature proof keyed by attestation data root. + * Cache an individual gossip signature keyed by validator and attestation root. * * @param client Client instance (state_lock must be held) - * @param data_root Attestation data root for the proof - * @param proof Aggregated signature proof to cache + * @param key Signature cache key + * @param signature XMSS signature to cache * @return 0 on success, -1 on error * * @note Thread safety: Caller must hold state_lock. */ -int lantern_client_agg_proof_cache_add( +int lantern_client_set_gossip_signature( + struct lantern_client *client, + const LanternSignatureKey *key, + const LanternAttestationData *data, + const LanternSignature *signature, + uint64_t target_slot); + +/** + * Cache a newly received aggregated signature proof keyed by attestation data root. + * + * @note Thread safety: Caller must hold state_lock. + */ +int lantern_client_add_new_aggregated_payload( struct lantern_client *client, const LanternRoot *data_root, + const LanternAttestationData *data, const LanternAggregatedSignatureProof *proof, uint64_t target_slot); /** - * Prune cached aggregated proofs whose attestation target slot is finalized. + * Cache a processed aggregated signature proof in the known payload pool. + * + * @note Thread safety: Caller must hold state_lock. + */ +int lantern_client_add_known_aggregated_payload( + struct lantern_client *client, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot); + +/** + * Promote pending aggregated payloads into the known pool. + * + * @note Thread safety: Caller must hold state_lock. + */ +size_t lantern_client_promote_new_aggregated_payloads( + struct lantern_client *client); + +/** + * Prune cached signatures, attestation data, and aggregated payloads whose + * attestation target slot is finalized. * * @param client Client instance (state_lock must be held) * @param finalized_slot Latest finalized slot boundary @@ -211,12 +245,56 @@ int lantern_client_agg_proof_cache_add( * * @note Thread safety: Caller must hold state_lock. */ -size_t lantern_client_agg_proof_cache_prune_finalized( +size_t lantern_client_prune_finalized_attestation_material( struct lantern_client *client, uint64_t finalized_slot); /** - * Aggregate block/subnet attestations into grouped proofs with cache reuse. + * Advance fork choice by exactly one interval and sync crossed payload-pool transitions. + * + * Caller must hold state_lock or otherwise guarantee exclusive access to the + * client store and fork-choice state. + */ +int lantern_client_tick_fork_choice_interval_locked( + struct lantern_client *client, + bool has_proposal); + +/** + * Move fork choice time directly to a later interval without replaying skipped intervals. + * + * Caller must hold state_lock or otherwise guarantee exclusive access to the + * client store and fork-choice state. + */ +int lantern_client_skip_fork_choice_intervals_locked( + struct lantern_client *client, + uint64_t target_interval); + +/** + * Catch fork choice up to a target interval using ChainService-style skip and yield semantics. + * + * The helper may release state_lock between interval ticks so other threads can + * process gossip and block imports while catch-up is in progress. + */ +int lantern_client_chain_service_tick_to( + struct lantern_client *client, + uint64_t target_interval, + bool has_proposal, + uint64_t *out_skipped_to_interval, + uint64_t *out_ticked_intervals); + +/** + * Advance fork choice time and sync the aggregated payload pools for crossed intervals. + * + * Caller must hold state_lock or otherwise guarantee exclusive access to the + * client store and fork-choice state. + */ +int lantern_client_advance_fork_choice_time_locked( + struct lantern_client *client, + uint64_t now_milliseconds, + bool has_proposal); + +/** + * Select cached aggregated proofs for block attestations. * * @note Thread safety: Acquires state_lock internally. */ diff --git a/src/core/client_services_internal.h b/src/core/client_services_internal.h index 8166f41..c580900 100644 --- a/src/core/client_services_internal.h +++ b/src/core/client_services_internal.h @@ -204,6 +204,41 @@ int validator_propose_block(struct lantern_client *client, uint64_t slot, size_t int validator_publish_attestations(struct lantern_client *client, uint64_t slot); +/** + * Timing service thread function. + * + * @param arg Client instance + * @return NULL + * + * @note Thread safety: This function runs in a separate thread + */ +void *timing_thread(void *arg); + + +/** + * Start the timing service. + * + * @param client Client instance + * @return LANTERN_CLIENT_OK on success or when already running/missing + * prerequisites + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if client is NULL + * @return LANTERN_CLIENT_ERR_RUNTIME if the service thread cannot be created + * + * @note Thread safety: This function is thread-safe + */ +int start_timing_service(struct lantern_client *client); + + +/** + * Stop the timing service. + * + * @param client Client instance + * + * @note Thread safety: This function is thread-safe + */ +void stop_timing_service(struct lantern_client *client); + + /** * Validator service thread function. * diff --git a/src/core/client_sync.c b/src/core/client_sync.c index 78fbed5..11903da 100644 --- a/src/core/client_sync.c +++ b/src/core/client_sync.c @@ -27,6 +27,7 @@ #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" #include "lantern/consensus/signature.h" +#include "lantern/consensus/ssz.h" #include "lantern/consensus/state.h" #include "lantern/storage/storage.h" #include "lantern/support/log.h" @@ -326,7 +327,8 @@ static bool verify_and_cache_aggregated_attestation_locked( size_t validator_count = lantern_state_validator_count(sig_state); size_t bit_length = attestation->proof.participants.bit_length; if (bit_length > validator_count) { - bit_length = validator_count; + lantern_state_reset(&target_state); + return false; } size_t participant_count = 0; for (size_t i = 0; i < bit_length; ++i) { @@ -376,9 +378,10 @@ static bool verify_and_cache_aggregated_attestation_locked( if (!verified) { return false; } - if (lantern_client_agg_proof_cache_add( + if (lantern_client_add_new_aggregated_payload( client, &data_root, + &attestation->data, &attestation->proof, attestation->data.target.slot) != 0) { @@ -674,7 +677,19 @@ int initialize_fork_choice(struct lantern_client *client) * We compute anchor_root from a header with the ACTUAL state_root, * matching Zeam's genStateBlockHeader() behavior. */ + lantern_store_attach_fork_choice(&client->store, &client->fork_choice); lantern_fork_choice_reset(&client->fork_choice); + if (lantern_store_prepare_fork_choice_votes( + &client->store, + client->state.config.num_validators) + != 0) + { + lantern_log_error( + "forkchoice", + &meta, + "failed to prepare fork choice votes"); + return LANTERN_CLIENT_ERR_RUNTIME; + } if (lantern_fork_choice_configure(&client->fork_choice, &client->state.config) != 0) { lantern_log_error( @@ -725,12 +740,13 @@ int initialize_fork_choice(struct lantern_client *client) * Real persisted checkpoints are synced to the store AFTER * restore_persisted_blocks() via lantern_fork_choice_restore_checkpoints(). */ - if (lantern_fork_choice_set_anchor( + if (lantern_fork_choice_set_anchor_with_state( &client->fork_choice, &anchor, &anchor_checkpoint, &anchor_checkpoint, - &anchor_root) + &anchor_root, + &client->state) != 0) { lantern_block_body_reset(&anchor.body); @@ -756,7 +772,6 @@ int initialize_fork_choice(struct lantern_client *client) } } lantern_block_body_reset(&anchor.body); - lantern_state_attach_fork_choice(&client->state, &client->fork_choice); client->has_fork_choice = true; return LANTERN_CLIENT_OK; } @@ -814,6 +829,41 @@ static int compare_blocks_by_slot(const void *lhs_ptr, const void *rhs_ptr) return memcmp(lhs->root.bytes, rhs->root.bytes, LANTERN_ROOT_SIZE); } +static bool load_restored_block_state( + const struct lantern_client *client, + const LanternRoot *root, + LanternState *out_state) +{ + if (!client || !root || !out_state || !client->data_dir || client->data_dir[0] == '\0') + { + return false; + } + + uint8_t *state_bytes = NULL; + size_t state_len = 0; + if (lantern_storage_load_state_bytes_for_root( + client->data_dir, + root, + &state_bytes, + &state_len) + != 0 + || !state_bytes + || state_len == 0) + { + free(state_bytes); + return false; + } + + lantern_state_init(out_state); + bool loaded = lantern_ssz_decode_state(out_state, state_bytes, state_len) == 0; + free(state_bytes); + if (!loaded) + { + lantern_state_reset(out_state); + } + return loaded; +} + /** * Restore persisted blocks from storage into fork choice. * @@ -862,8 +912,12 @@ int restore_persisted_blocks(struct lantern_client *client) const struct lantern_persisted_block *entry = &list.items[i]; const LanternBlock *block = &entry->block.message.block; const LanternVote *vote = &entry->block.message.proposer_attestation; + LanternState cached_post_state; + bool have_cached_post_state = load_restored_block_state(client, &entry->root, &cached_post_state); LanternSignedVote persisted_proposer; const LanternSignedVote *proposer_ptr = NULL; + const LanternCheckpoint *post_justified = &client->state.latest_justified; + const LanternCheckpoint *post_finalized = &client->state.latest_finalized; if (vote->slot == block->slot && vote->validator_id == block->proposer_index) { memset(&persisted_proposer, 0, sizeof(persisted_proposer)); @@ -871,13 +925,19 @@ int restore_persisted_blocks(struct lantern_client *client) persisted_proposer.signature = entry->block.signatures.proposer_signature; proposer_ptr = &persisted_proposer; } - if (lantern_fork_choice_add_block( + if (have_cached_post_state) + { + post_justified = &cached_post_state.latest_justified; + post_finalized = &cached_post_state.latest_finalized; + } + if (lantern_fork_choice_add_block_with_state( &client->fork_choice, block, proposer_ptr, - &client->state.latest_justified, - &client->state.latest_finalized, - &entry->root) + post_justified, + post_finalized, + &entry->root, + have_cached_post_state ? &cached_post_state : NULL) != 0) { lantern_log_warn( @@ -886,10 +946,14 @@ int restore_persisted_blocks(struct lantern_client *client) "failed to restore block at slot %" PRIu64, entry->block.message.block.slot); } + if (have_cached_post_state) + { + lantern_state_reset(&cached_post_state); + } } uint64_t now_milliseconds = validator_wall_time_now_millis(); - if (lantern_fork_choice_advance_time(&client->fork_choice, now_milliseconds, false) != 0) + if (lantern_client_advance_fork_choice_time_locked(client, now_milliseconds, false) != 0) { lantern_log_warn( "forkchoice", diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index 72387e8..4c06349 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -46,98 +46,6 @@ enum ROOT_HEX_BUFFER_LEN = (LANTERN_ROOT_SIZE * 2u) + 3u, }; -#define LANTERN_STORAGE_STATE_META_VERSION 1u - -struct lantern_state_meta_wire -{ - uint32_t version; - uint32_t reserved; - uint64_t historical_roots_offset; - uint64_t justified_slots_offset; -}; - -static int load_state_meta_for_root( - const char *data_dir, - const LanternRoot *root, - struct lantern_state_meta_wire *out_meta) -{ - if (!data_dir || !root || !out_meta) - { - return -1; - } - - char root_hex[(2u * LANTERN_ROOT_SIZE) + 1u]; - if (lantern_bytes_to_hex(root->bytes, LANTERN_ROOT_SIZE, root_hex, sizeof(root_hex), 0) != 0) - { - return -1; - } - - const size_t base_len = strlen(data_dir); - const bool needs_sep = - base_len > 0 && data_dir[base_len - 1u] != '/' && data_dir[base_len - 1u] != '\\'; - const size_t path_len = - base_len + (needs_sep ? 1u : 0u) + strlen("states/") + strlen(root_hex) + strlen(".meta") + 1u; - char *meta_path = malloc(path_len); - if (!meta_path) - { - return -1; - } - const int path_written = snprintf( - meta_path, - path_len, - "%s%sstates/%s.meta", - data_dir, - needs_sep ? "/" : "", - root_hex); - if (path_written < 0 || (size_t)path_written >= path_len) - { - free(meta_path); - return -1; - } - - int rc = -1; - FILE *fp = fopen(meta_path, "rb"); - if (!fp) - { - free(meta_path); - return (errno == ENOENT) ? 1 : -1; - } - - if (fseek(fp, 0, SEEK_END) != 0) - { - goto cleanup; - } - long file_size = ftell(fp); - if (file_size < 0 || (size_t)file_size != sizeof(*out_meta)) - { - if (file_size == 0) - { - rc = 1; - } - goto cleanup; - } - if (fseek(fp, 0, SEEK_SET) != 0) - { - goto cleanup; - } - - if (fread(out_meta, 1u, sizeof(*out_meta), fp) != sizeof(*out_meta)) - { - goto cleanup; - } - if (out_meta->version != LANTERN_STORAGE_STATE_META_VERSION) - { - goto cleanup; - } - - rc = 0; - -cleanup: - fclose(fp); - free(meta_path); - return rc; -} - static int prepare_off_head_snapshot_state( const char *data_dir, const LanternRoot *root, @@ -148,33 +56,6 @@ static int prepare_off_head_snapshot_state( return -1; } - struct lantern_state_meta_wire meta = {0}; - const int meta_rc = load_state_meta_for_root(data_dir, root, &meta); - if (meta_rc == 0) - { - state->historical_roots_offset = meta.historical_roots_offset; - state->justified_slots_offset = meta.justified_slots_offset; - } - else if (meta_rc == 1) - { - state->historical_roots_offset = 0; - state->justified_slots_offset = - state->latest_finalized.slot == UINT64_MAX ? 0u : (state->latest_finalized.slot + 1u); - } - else - { - return -1; - } - - if (state->config.num_validators == 0) - { - return -1; - } - if (lantern_state_prepare_validator_votes(state, state->config.num_validators) != 0) - { - return -1; - } - return 0; } @@ -332,8 +213,6 @@ static bool signed_block_signatures_are_valid( const LanternAggregatedAttestations *attestations = &block->message.block.body.attestations; const LanternAttestationSignatures *sig_groups = &block->signatures.attestation_signatures; size_t att_count = attestations->length; - bool missing_attestation_signature_payload = - (att_count > 0) && (sig_groups->length == 0); if (!client->genesis.validator_registry.records) { @@ -353,44 +232,32 @@ static bool signed_block_signatures_are_valid( } if (att_count != sig_groups->length) { - if (!missing_attestation_signature_payload) - { - lantern_log_warn( - "state", - meta, - "signed block slot=%" PRIu64 " aggregated signature count mismatch expected=%zu actual=%zu", - block->message.block.slot, - att_count, - sig_groups->length); - lantern_state_reset(&parent_state); - return false; - } lantern_log_warn( "state", meta, - "signed block slot=%" PRIu64 " missing attestation signatures payload; accepting legacy compatibility path attestation_count=%zu", + "signed block slot=%" PRIu64 " aggregated signature count mismatch expected=%zu actual=%zu", block->message.block.slot, - att_count); + att_count, + sig_groups->length); + lantern_state_reset(&parent_state); + return false; } if (att_count > 0 && !sig_groups->data) { - if (!missing_attestation_signature_payload) - { - lantern_log_warn( - "state", - meta, - "signed block slot=%" PRIu64 " missing aggregated signatures length=%zu", - block->message.block.slot, - sig_groups->length); - lantern_state_reset(&parent_state); - return false; - } + lantern_log_warn( + "state", + meta, + "signed block slot=%" PRIu64 " missing aggregated signatures length=%zu", + block->message.block.slot, + sig_groups->length); + lantern_state_reset(&parent_state); + return false; } size_t validator_count = lantern_state_validator_count(state_for_sig); const uint8_t **pubkeys = NULL; - for (size_t i = 0; i < att_count && !missing_attestation_signature_payload; ++i) + for (size_t i = 0; i < att_count; ++i) { const LanternAggregatedAttestation *att = &attestations->data[i]; const LanternAggregatedSignatureProof *proof = &sig_groups->data[i]; @@ -481,23 +348,13 @@ static bool signed_block_signatures_are_valid( bool proposer_signature_present = !lantern_signature_is_zero(&block->signatures.proposer_signature); if (!proposer_signature_present) { - if (!missing_attestation_signature_payload) - { - lantern_log_warn( - "state", - meta, - "signed block slot=%" PRIu64 " missing proposer signature", - block->message.block.slot); - lantern_state_reset(&parent_state); - return false; - } lantern_log_warn( "state", meta, - "signed block slot=%" PRIu64 " missing proposer signature; accepting legacy compatibility path", + "signed block slot=%" PRIu64 " missing proposer signature", block->message.block.slot); lantern_state_reset(&parent_state); - return true; + return false; } LanternSignedVote proposer_signed = {0}; @@ -538,9 +395,10 @@ 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_agg_proof_cache_add( + (void)lantern_client_add_new_aggregated_payload( client, &data_root, + &attestations->data[i].data, &proofs->data[i], attestations->data[i].data.target.slot); } @@ -1087,6 +945,7 @@ static bool init_replay_state(const struct lantern_client *client, LanternState } struct lantern_log_metadata meta = {.validator = client->node_id}; + lantern_state_reset(out_state); lantern_state_init(out_state); if (client->genesis.state_bytes && client->genesis.state_size > 0) @@ -1182,6 +1041,12 @@ static bool rebuild_state_for_root_locked( LanternRoot *out_missing_roots, size_t missing_roots_cap, size_t *out_missing_count); +static void cache_rebuilt_state_for_root_locked( + struct lantern_client *client, + const LanternRoot *root, + const LanternState *state, + const struct lantern_log_metadata *meta, + const char *context); static bool state_matches_root(const LanternState *state, const LanternRoot *root) { @@ -1241,6 +1106,7 @@ static bool load_snapshot_state_for_root_locked( if (lantern_ssz_decode_state(&decoded, state_bytes, state_len) == 0 && prepare_off_head_snapshot_state(client->data_dir, root, &decoded) == 0) { + lantern_state_reset(out_state); *out_state = decoded; decoded_owned = false; loaded = true; @@ -1290,6 +1156,20 @@ static bool load_replay_base_from_finalized_locked( } } + if (client->has_fork_choice) + { + const LanternState *cached_state = + lantern_fork_choice_block_state(&client->fork_choice, finalized_root); + if (cached_state && lantern_state_clone(cached_state, out_state) == 0) + { + if (out_source) + { + *out_source = "fork_choice_cache"; + } + return true; + } + } + if (client->data_dir && client->data_dir[0] != '\0') { LanternState persisted_finalized; @@ -1310,6 +1190,12 @@ static bool load_replay_base_from_finalized_locked( size_t snapshot_len = 0; if (load_snapshot_state_for_root_locked(client, finalized_root, out_state, &snapshot_len)) { + cache_rebuilt_state_for_root_locked( + client, + finalized_root, + out_state, + NULL, + "finalized_base_snapshot"); if (out_source) { *out_source = "state_for_root"; @@ -1342,13 +1228,34 @@ static bool load_replay_base_from_finalized_locked( } static void cache_rebuilt_state_for_root_locked( - const struct lantern_client *client, + struct lantern_client *client, const LanternRoot *root, const LanternState *state, const struct lantern_log_metadata *meta, const char *context) { - if (!client || !root || !state || !client->data_dir || !client->data_dir[0]) + if (!client || !root || !state) + { + return; + } + + if (client->has_fork_choice + && lantern_fork_choice_set_block_state(&client->fork_choice, root, state) != 0) + { + char root_hex[ROOT_HEX_BUFFER_LEN]; + struct lantern_log_metadata fallback_meta = {.validator = client->node_id}; + const struct lantern_log_metadata *log_meta = meta ? meta : &fallback_meta; + format_root_hex(root, root_hex, sizeof(root_hex)); + lantern_log_warn( + "forkchoice", + log_meta, + "failed to cache in-memory state root=%s slot=%" PRIu64 " context=%s", + root_hex[0] ? root_hex : "0x0", + state->slot, + context ? context : "unknown"); + } + + if (!client->data_dir || !client->data_dir[0]) { return; } @@ -1389,6 +1296,16 @@ const LanternState *lantern_client_state_for_root_locked( return &client->state; } + if (client->has_fork_choice) + { + const LanternState *cached_state = + lantern_fork_choice_block_state(&client->fork_choice, root); + if (cached_state) + { + return cached_state; + } + } + if (!scratch) { return NULL; @@ -1414,6 +1331,12 @@ const LanternState *lantern_client_state_for_root_locked( if (prepare_off_head_snapshot_state(client->data_dir, root, scratch) == 0) { free(state_bytes); + cache_rebuilt_state_for_root_locked( + client, + root, + scratch, + NULL, + "state_for_root_snapshot"); if (out_is_scratch) { *out_is_scratch = true; @@ -1431,12 +1354,6 @@ const LanternState *lantern_client_state_for_root_locked( lantern_state_reset(scratch); if (rebuild_state_for_root_locked(client, root, scratch, NULL, 0, NULL)) { - cache_rebuilt_state_for_root_locked( - client, - root, - scratch, - NULL, - "state_for_root"); if (out_is_scratch) { *out_is_scratch = true; @@ -1457,7 +1374,6 @@ static void adopt_state_locked(struct lantern_client *client, LanternState *stat } LanternState previous = client->state; client->state = *state; - lantern_state_attach_fork_choice(&client->state, &client->fork_choice); if (client->has_fork_choice) { if (lantern_fork_choice_update_checkpoints( @@ -1488,6 +1404,7 @@ static bool rebuild_state_for_root_locked( { return false; } + lantern_state_reset(out_state); if (out_missing_count) { *out_missing_count = 0; @@ -1497,6 +1414,30 @@ static bool rebuild_state_for_root_locked( char target_hex[ROOT_HEX_BUFFER_LEN]; format_root_hex(target_root, target_hex, sizeof(target_hex)); + if (client->has_fork_choice) + { + const LanternState *cached_state = + lantern_fork_choice_block_state(&client->fork_choice, target_root); + if (cached_state) + { + if (lantern_state_clone(cached_state, out_state) == 0) + { + lantern_log_debug( + "state", + &meta, + "rebuild_state used fork-choice cache target=%s", + target_hex[0] ? target_hex : "0x0"); + return true; + } + lantern_state_reset(out_state); + lantern_log_warn( + "state", + &meta, + "rebuild_state failed to clone fork-choice cache target=%s", + target_hex[0] ? target_hex : "0x0"); + } + } + /* * Fast path: if a post-state snapshot exists for this root, use it * directly. This is required for checkpoint-sync anchors whose ancestors @@ -1521,8 +1462,15 @@ static bool rebuild_state_for_root_locked( { if (prepare_off_head_snapshot_state(client->data_dir, target_root, &snapshot) == 0) { + lantern_state_reset(out_state); *out_state = snapshot; free(state_bytes); + cache_rebuilt_state_for_root_locked( + client, + target_root, + out_state, + &meta, + "rebuild_snapshot"); lantern_log_debug( "state", &meta, @@ -1782,6 +1730,7 @@ static bool rebuild_state_for_root_locked( bool success = false; if (use_finalized_shortcut) { + lantern_state_reset(out_state); *out_state = replay_base_state; lantern_state_init(&replay_base_state); have_replay_base_state = false; @@ -1808,9 +1757,24 @@ static bool rebuild_state_for_root_locked( return false; } + LanternStore replay_store; + lantern_store_init(&replay_store); + if (lantern_store_prepare_validator_votes(&replay_store, out_state->config.num_validators) != 0) + { + lantern_signed_block_list_reset(&response); + root_chain_reset(&chain); + lantern_state_reset(out_state); + if (have_replay_base_state) + { + lantern_state_reset(&replay_base_state); + } + lantern_store_reset(&replay_store); + return false; + } + for (size_t i = 0; i < response.length; ++i) { - int transition_rc = lantern_state_transition(out_state, &response.blocks[i]); + int transition_rc = lantern_state_transition(out_state, &replay_store, &response.blocks[i]); if (transition_rc != 0) { LanternRoot block_root = {0}; @@ -1834,16 +1798,24 @@ static bool rebuild_state_for_root_locked( { lantern_state_reset(&replay_base_state); } + lantern_store_reset(&replay_store); return false; } } + lantern_store_reset(&replay_store); lantern_signed_block_list_reset(&response); root_chain_reset(&chain); if (have_replay_base_state) { lantern_state_reset(&replay_base_state); } + cache_rebuilt_state_for_root_locked( + client, + target_root, + out_state, + &meta, + "rebuild_state"); return true; } @@ -1949,6 +1921,7 @@ static bool add_competing_fork_block_locked( struct lantern_client *client, const LanternSignedBlock *block, const LanternRoot *block_root, + const LanternState *post_state, const LanternCheckpoint *post_justified, const LanternCheckpoint *post_finalized, const struct lantern_log_metadata *meta) @@ -1962,13 +1935,14 @@ static bool add_competing_fork_block_locked( proposer_signed.data = block->message.proposer_attestation; proposer_signed.signature = block->signatures.proposer_signature; - if (lantern_fork_choice_add_block( + if (lantern_fork_choice_add_block_with_state( &client->fork_choice, &block->message.block, &proposer_signed, post_justified, post_finalized, - block_root) != 0) + block_root, + post_state) != 0) { return false; } @@ -2042,13 +2016,10 @@ static bool validate_block_vote_constraints_locked( &rejection)) { /* - * Block attestations are advisory for fork-choice vote tracking. - * Do not fail block import here: state transition remains the - * consensus validity gate, and lantern_fork_choice_add_block() - * already best-effort applies block votes. - * - * This avoids deadlocking checkpoint-sync catch-up when - * attestations reference roots not yet restored locally. + * Block-body attestations only affect local fork-choice vote + * tracking. A valid block can carry attestations that reference + * roots we have not restored locally yet, so skip those votes and + * let block import continue. */ skipped_constraints += 1u; if (rejection.has_unknown_root) @@ -2061,7 +2032,7 @@ static bool validate_block_vote_constraints_locked( lantern_log_debug( "state", meta, - "ignoring block attestation unknown root=%s slot=%" PRIu64 + "skipping block attestation unknown root=%s slot=%" PRIu64 " block_slot=%" PRIu64, unknown_hex[0] ? unknown_hex : "0x0", rejection.unknown_slot, @@ -2072,7 +2043,7 @@ static bool validate_block_vote_constraints_locked( lantern_log_debug( "state", meta, - "ignoring block attestation constraint failure block_slot=%" PRIu64 + "skipping block attestation constraint failure block_slot=%" PRIu64 " reason=%s", block->message.block.slot, rejection.message); @@ -2121,7 +2092,7 @@ static bool apply_state_transition_locked( } LanternSignedBlock import_block = *block; - if (lantern_state_transition(&client->state, &import_block) != 0) + if (lantern_state_transition(&client->state, &client->store, &import_block) != 0) { lantern_log_warn( "state", @@ -2155,7 +2126,7 @@ static void advance_fork_choice_time_locked( } uint64_t now_milliseconds = validator_wall_time_now_millis(); - if (lantern_fork_choice_advance_time(&client->fork_choice, now_milliseconds, false) != 0) + if (lantern_client_advance_fork_choice_time_locked(client, now_milliseconds, false) != 0) { lantern_log_debug( "forkchoice", @@ -2238,6 +2209,37 @@ static bool finalized_checkpoint_advanced( return false; } +static bool finalized_slot_advanced( + const LanternCheckpoint *previous_finalized, + const LanternCheckpoint *current_finalized) +{ + if (!previous_finalized || !current_finalized) + { + return false; + } + return current_finalized->slot > previous_finalized->slot; +} + +static void prune_finalized_attestation_material_if_slot_advanced_locked( + struct lantern_client *client, + const LanternCheckpoint *previous_finalized) +{ + if (!client || !previous_finalized) + { + return; + } + + const LanternCheckpoint *current_finalized = &client->state.latest_finalized; + if (!finalized_slot_advanced(previous_finalized, current_finalized)) + { + return; + } + + (void)lantern_client_prune_finalized_attestation_material( + client, + current_finalized->slot); +} + static void persist_finalized_state_if_advanced_locked( const struct lantern_client *client, const LanternCheckpoint *previous_finalized, @@ -2308,7 +2310,7 @@ static void persist_state_locked( "failed to persist state after slot=%" PRIu64, client->state.slot); } - if (lantern_storage_save_votes(client->data_dir, &client->state) != 0) + if (lantern_storage_save_votes(client->data_dir, &client->state, &client->store) != 0) { lantern_log_warn( "storage", @@ -2632,12 +2634,11 @@ bool lantern_client_import_block( goto cleanup; } - cache_block_aggregated_proofs_locked(client, block); - if (parent_off_head) { LanternRoot parent_root = block->message.block.parent_root; LanternState replay_state; + lantern_state_init(&replay_state); bool have_replay_state = false; bool processed = false; bool deferred = false; @@ -2653,18 +2654,18 @@ bool lantern_client_import_block( &missing_count)) { have_replay_state = true; - cache_rebuilt_state_for_root_locked( - client, - &parent_root, - &replay_state, - meta, - "off_head_parent"); - if (lantern_state_transition(&replay_state, block) == 0) + LanternStore replay_store; + lantern_store_init(&replay_store); + bool replay_store_ready = + lantern_store_prepare_validator_votes(&replay_store, replay_state.config.num_validators) == 0; + if (replay_store_ready + && lantern_state_transition(&replay_state, &replay_store, block) == 0) { processed = add_competing_fork_block_locked( client, block, &block_root_local, + &replay_state, &replay_state.latest_justified, &replay_state.latest_finalized, meta); @@ -2684,6 +2685,7 @@ bool lantern_client_import_block( "off-head state transition failed for slot=%" PRIu64, block->message.block.slot); } + lantern_store_reset(&replay_store); } else { @@ -2750,6 +2752,7 @@ bool lantern_client_import_block( else { LanternState head_state; + lantern_state_init(&head_state); if (rebuild_state_for_root_locked( client, &fork_head, @@ -2761,6 +2764,10 @@ bool lantern_client_import_block( adopt_state_locked(client, &head_state); adopted_state = true; } + else + { + lantern_state_reset(&head_state); + } } } } @@ -2771,9 +2778,9 @@ bool lantern_client_import_block( client, &pre_adopt_finalized, meta); - (void)lantern_client_agg_proof_cache_prune_finalized( + prune_finalized_attestation_material_if_slot_advanced_locked( client, - client->state.latest_finalized.slot); + &pre_adopt_finalized); persist_state_locked(client, meta); } @@ -2823,13 +2830,15 @@ bool lantern_client_import_block( goto cleanup; } + cache_block_aggregated_proofs_locked(client, block); + persist_finalized_state_if_advanced_locked( client, &pre_transition_finalized, meta); - (void)lantern_client_agg_proof_cache_prune_finalized( + prune_finalized_attestation_material_if_slot_advanced_locked( client, - client->state.latest_finalized.slot); + &pre_transition_finalized); advance_fork_choice_time_locked(client, block, meta); get_head_info_locked(client, &head_root, &head_slot); persist_state_locked(client, meta); diff --git a/src/core/client_sync_votes.c b/src/core/client_sync_votes.c index f82faa8..60ddf57 100644 --- a/src/core/client_sync_votes.c +++ b/src/core/client_sync_votes.c @@ -39,10 +39,6 @@ enum * External Functions (from client_sync.c) * ============================================================================ */ -extern const struct lantern_validator_record *lantern_client_get_validator_record( - const struct lantern_client *client, - uint64_t validator_id); - bool lantern_client_verify_vote_signature( const struct lantern_client *client, const LanternSignedVote *vote, @@ -188,8 +184,8 @@ static bool validate_vote_cache_state( } uint64_t validator_count = client->state.config.num_validators; - bool cache_available = (validator_count != 0) && client->state.validator_votes - && (client->state.validator_votes_len != 0); + bool cache_available = (validator_count != 0) && client->store.validator_votes + && (client->store.validator_votes_len != 0); if (!cache_available) { lantern_log_debug( @@ -204,7 +200,7 @@ static bool validate_vote_cache_state( } bool validator_in_range = (vote->validator_id < validator_count) - && (vote->validator_id < (uint64_t)client->state.validator_votes_len); + && (vote->validator_id < (uint64_t)client->store.validator_votes_len); if (!validator_in_range) { lantern_log_debug( @@ -247,8 +243,8 @@ static bool cache_state_vote_locked( return false; } - int result = lantern_state_set_signed_validator_vote( - &client->state, + int result = lantern_store_set_signed_validator_vote( + &client->store, (size_t)vote->data.validator_id, vote); if (result != 0) @@ -271,66 +267,6 @@ static bool cache_state_vote_locked( } -/** - * @brief Apply a vote to fork choice and advance time. - * - * @param client Client instance - * @param vote Signed vote to apply - * @param meta Logging metadata - * - * @note Thread safety: Caller must hold state_lock - */ -static void apply_vote_to_fork_choice_locked( - struct lantern_client *client, - const LanternSignedVote *vote, - const struct lantern_log_metadata *meta) -{ - if (!client || !vote || !meta || !client->has_fork_choice) - { - return; - } - - int result = lantern_fork_choice_add_vote(&client->fork_choice, vote, false); - if (result != 0) - { - lantern_log_debug( - "forkchoice", - meta, - "failed to track gossip vote validator=%" PRIu64 " slot=%" PRIu64, - vote->data.validator_id, - vote->data.slot); - return; - } - - if (client->debug_disable_fork_choice_time) - { - return; - } - - uint64_t now_millis = 0; - uint64_t vote_time_seconds = 0; - if (lantern_client_vote_time_seconds(client, vote->data.slot, &vote_time_seconds)) - { - now_millis = vote_time_seconds * 1000u; - } - else - { - now_millis = validator_wall_time_now_millis(); - } - - result = lantern_fork_choice_advance_time(&client->fork_choice, now_millis, false); - if (result != 0) - { - lantern_log_debug( - "forkchoice", - meta, - "advancing fork choice time failed after validator=%" PRIu64 " slot=%" PRIu64, - vote->data.validator_id, - vote->data.slot); - } -} - - /** * @brief Persist votes to storage if configured. * @@ -350,7 +286,7 @@ static void persist_votes_if_configured_locked( return; } - if (lantern_storage_save_votes(client->data_dir, &client->state) != 0) + if (lantern_storage_save_votes(client->data_dir, &client->state, &client->store) != 0) { lantern_log_warn( "storage", @@ -421,10 +357,17 @@ static bool process_vote_locked( lantern_log_debug( "gossip", meta, - "missing target state root=%s for validator=%" PRIu64 " slot=%" PRIu64 " (using current state)", + "missing target state root=%s for validator=%" PRIu64 " slot=%" PRIu64, target_hex[0] ? target_hex : "0x0", vote->data.validator_id, vote->data.slot); + lantern_vote_rejection_set( + rejection, + "missing target state target_slot=%" PRIu64 " root=%s", + vote->data.target.slot, + target_hex[0] ? target_hex : "0x0"); + lantern_state_reset(&target_state); + return false; } if (!lantern_client_verify_vote_signature( @@ -457,7 +400,30 @@ static bool process_vote_locked( return false; } - apply_vote_to_fork_choice_locked(client, vote, meta); + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote->data.data, &data_root) == 0) + { + LanternSignatureKey key = { + .validator_index = vote->data.validator_id, + .data_root = data_root, + }; + if (lantern_client_set_gossip_signature( + client, + &key, + &vote->data.data, + &vote->signature, + vote->data.target.slot) + != 0) + { + lantern_log_debug( + "state", + meta, + "failed to cache gossip signature validator=%" PRIu64 " slot=%" PRIu64, + vote->data.validator_id, + vote->data.slot); + } + } + persist_votes_if_configured_locked(client, vote, meta); return true; } @@ -472,9 +438,9 @@ static bool process_vote_locked( * * @spec subspecs/xmss/signature.py - XMSS signature verification * - * Retrieves the validator's 52-byte public key from either the state's - * validator registry or the genesis registry as fallback, then verifies - * the XMSS signature over the vote's hash tree root. + * Retrieves the validator's 52-byte public key from the supplied state's + * validator registry, then verifies the XMSS signature over the vote's + * hash tree root. * * Per LeanSpec: Always use the 52-byte pubkey directly from state.validators[].pubkey. * This matches Zeam's verifyBincode which takes pubkey bytes directly from state. @@ -482,7 +448,7 @@ static bool process_vote_locked( * @param client Client instance * @param vote Signed vote to verify * @param signature Signature to verify - * @param state_override Optional state to use for validator pubkey lookup + * @param state_override State to use for validator pubkey lookup * @param meta Logging metadata * @param context Description of signature context for logging * @return true if signature is valid @@ -501,86 +467,47 @@ bool lantern_client_verify_vote_signature( { return false; } - const uint8_t *pubkey_bytes = NULL; - if (state_override) + if (!state_override) { - size_t state_validator_count = lantern_state_validator_count(state_override); - if (state_validator_count == 0) - { - lantern_log_warn( - "state", - meta, - "missing validator registry for %s signature verification", - context ? context : "vote"); - return false; - } - if (vote->data.validator_id >= state_validator_count) - { - lantern_log_warn( - "state", - meta, - "validator=%" PRIu64 " exceeds target state validator count=%zu", - vote->data.validator_id, - state_validator_count); - return false; - } - pubkey_bytes = lantern_state_validator_pubkey( - state_override, - (size_t)vote->data.validator_id); - if (!pubkey_bytes || lantern_validator_pubkey_is_zero(pubkey_bytes)) - { - lantern_log_warn( - "state", - meta, - "missing validator pubkey for %s signature verification", - context ? context : "vote"); - return false; - } + lantern_log_warn( + "state", + meta, + "missing state for %s signature verification", + context ? context : "vote"); + return false; } - else + size_t state_validator_count = lantern_state_validator_count(state_override); + if (state_validator_count == 0) { - bool state_has_registry = client->has_state; - size_t state_validator_count = 0; - if (state_has_registry) - { - state_validator_count = lantern_state_validator_count(&client->state); - } - if (state_has_registry && state_validator_count > 0) - { - if (vote->data.validator_id >= state_validator_count) - { - lantern_log_warn( - "state", - meta, - "validator=%" PRIu64 " exceeds parent state validator count=%zu", - vote->data.validator_id, - state_validator_count); - return false; - } - pubkey_bytes = lantern_state_validator_pubkey( - &client->state, - (size_t)vote->data.validator_id); - if (lantern_validator_pubkey_is_zero(pubkey_bytes)) - { - pubkey_bytes = NULL; - } - } - if (!pubkey_bytes) - { - const struct lantern_validator_record *record = - lantern_client_get_validator_record(client, vote->data.validator_id); - if (!record || !record->has_pubkey_bytes) - { - lantern_log_warn( - "state", - meta, - "missing validator %s pubkey for validator=%" PRIu64, - context ? context : "signature", - vote->data.validator_id); - return false; - } - pubkey_bytes = record->pubkey_bytes; - } + lantern_log_warn( + "state", + meta, + "missing validator registry for %s signature verification", + context ? context : "vote"); + return false; + } + if (vote->data.validator_id >= state_validator_count) + { + lantern_log_warn( + "state", + meta, + "validator=%" PRIu64 " exceeds %s state validator count=%zu", + vote->data.validator_id, + context ? context : "vote", + state_validator_count); + return false; + } + const uint8_t *pubkey_bytes = lantern_state_validator_pubkey( + state_override, + (size_t)vote->data.validator_id); + if (!pubkey_bytes || lantern_validator_pubkey_is_zero(pubkey_bytes)) + { + lantern_log_warn( + "state", + meta, + "missing validator pubkey for %s signature verification", + context ? context : "vote"); + return false; } LanternRoot vote_root; if (lantern_hash_tree_root_attestation_data(&vote->data.data, &vote_root) != 0) @@ -795,8 +722,8 @@ bool lantern_client_validate_vote_constraints( * 1. Validates vote constraints (checkpoints, slots) * 2. Verifies XMSS signature * 3. Validates vote cache availability and validator range - * 4. Updates fork choice with the vote - * 5. Caches vote in state + * 4. Caches the signed vote in state + * 5. Stores gossip signature/data for the staged aggregation pipeline * 6. Persists votes to storage * * @param client Client instance diff --git a/src/core/client_validator.c b/src/core/client_validator.c index 9d4911a..ff6ce3f 100644 --- a/src/core/client_validator.c +++ b/src/core/client_validator.c @@ -18,6 +18,11 @@ #include #include +#if defined(_WIN32) +#include +#else +#include +#endif #include #include @@ -37,6 +42,12 @@ * Constants * ============================================================================ */ +/** Sleep interval when timing service cannot run (ms). */ +static const uint32_t TIMING_SERVICE_IDLE_SLEEP_MS = 200; + +/** Sleep interval after timing service errors (ms). */ +static const uint32_t TIMING_SERVICE_POLL_SLEEP_MS = 50; + /** Sleep interval when validator service cannot run (ms). */ static const uint32_t VALIDATOR_SERVICE_IDLE_SLEEP_MS = 200; @@ -61,6 +72,94 @@ static size_t validator_attestation_committee_count(const struct lantern_client static int validator_publish_aggregated_attestations(struct lantern_client *client, uint64_t slot); +static bool timing_service_should_run(const struct lantern_client *client) +{ + if (!client) { + return false; + } + if (client->debug_disable_fork_choice_time) { + return false; + } + if (!client->has_state || !client->has_runtime || !client->has_fork_choice) { + return false; + } + if (!client->fork_choice.initialized || !client->fork_choice.has_anchor) { + return false; + } + return true; +} + +static int timing_service_compute_target_interval( + const struct lantern_client *client, + uint64_t now_milliseconds, + uint64_t *out_target_interval, + uint64_t *out_genesis_milliseconds) +{ + if (!client || !client->has_runtime || !out_target_interval) { + return -1; + } + + const struct lantern_slot_clock *clock = &client->runtime.clock; + if (clock->milliseconds_per_interval == 0 || clock->genesis_time > UINT64_MAX / 1000u) { + return -1; + } + + uint64_t genesis_milliseconds = clock->genesis_time * 1000u; + if (out_genesis_milliseconds) { + *out_genesis_milliseconds = genesis_milliseconds; + } + if (now_milliseconds < genesis_milliseconds) { + return 1; + } + + *out_target_interval = + (now_milliseconds - genesis_milliseconds) / clock->milliseconds_per_interval; + return 0; +} + +static uint32_t timing_service_sleep_until_next_interval( + const struct lantern_client *client, + uint64_t now_milliseconds, + uint64_t target_interval) +{ + if (!client || !client->has_runtime || target_interval == UINT64_MAX) { + return TIMING_SERVICE_POLL_SLEEP_MS; + } + + const struct lantern_slot_clock *clock = &client->runtime.clock; + if (clock->milliseconds_per_interval == 0 || clock->genesis_time > UINT64_MAX / 1000u) { + return TIMING_SERVICE_POLL_SLEEP_MS; + } + + uint64_t genesis_milliseconds = clock->genesis_time * 1000u; + uint64_t next_interval = target_interval + 1u; + if (next_interval < target_interval + || next_interval > (UINT64_MAX - genesis_milliseconds) / clock->milliseconds_per_interval) { + return TIMING_SERVICE_POLL_SLEEP_MS; + } + + uint64_t next_interval_start = + genesis_milliseconds + (next_interval * clock->milliseconds_per_interval); + if (now_milliseconds >= next_interval_start) { + return 0u; + } + + uint64_t remaining = next_interval_start - now_milliseconds; + if (remaining > UINT32_MAX) { + remaining = UINT32_MAX; + } + return (uint32_t)remaining; +} + +static void timing_service_yield(void) +{ +#if defined(_WIN32) + Sleep(0); +#else + sched_yield(); +#endif +} + static bool validator_should_pause_for_sync(const struct lantern_client *client) { if (!client || !client->status_lock_initialized || !client->has_state) @@ -221,6 +320,18 @@ static bool validator_should_pause_for_sync(const struct lantern_client *client) return false; } +static void validator_clear_pending_attestation(struct lantern_local_validator *validator) +{ + if (!validator) + { + return; + } + + validator->has_pending_attestation = false; + validator->pending_attestation_slot = UINT64_MAX; + memset(&validator->pending_attestation, 0, sizeof(validator->pending_attestation)); +} + /* ============================================================================ * Aggregation Helpers * ============================================================================ */ @@ -728,7 +839,8 @@ static lantern_client_error append_cached_exact_group_proof( return LANTERN_CLIENT_OK; } - const struct lantern_agg_proof_cache *cache = &client->agg_proof_cache; + const struct lantern_aggregated_payload_pool *cache = + &client->store.known_aggregated_payloads; if (!cache->entries || cache->length == 0) { return LANTERN_CLIENT_OK; @@ -778,7 +890,8 @@ static lantern_client_error append_cached_proofs_for_group( { return LANTERN_CLIENT_OK; } - const struct lantern_agg_proof_cache *cache = &client->agg_proof_cache; + const struct lantern_aggregated_payload_pool *cache = + &client->store.known_aggregated_payloads; if (!cache->entries || cache->length == 0) { return LANTERN_CLIENT_OK; @@ -841,6 +954,7 @@ static lantern_client_error aggregate_attestations_for_block( struct lantern_client *client, const LanternAttestations *att_list, const LanternSignatureList *att_signatures, + bool allow_raw_signature_aggregation, LanternAggregatedAttestations *out_attestations, LanternAttestationSignatures *out_signatures) { @@ -920,7 +1034,7 @@ static lantern_client_error aggregate_attestations_for_block( rc = LANTERN_CLIENT_ERR_VALIDATOR; break; } - if (groups[i].count > 0) + if (allow_raw_signature_aggregation && groups[i].count > 0) { bool used_cached_group_proof = false; rc = append_cached_exact_group_proof( @@ -969,7 +1083,7 @@ static lantern_client_error aggregate_attestations_for_block( remaining_count += 1u; } } - if (rc == LANTERN_CLIENT_OK && groups[i].count > 0) + if (rc == LANTERN_CLIENT_OK && allow_raw_signature_aggregation && groups[i].count > 0) { for (size_t j = 0; j < groups[i].count; ++j) { @@ -1036,6 +1150,7 @@ lantern_client_error lantern_client_aggregate_attestations_for_block( client, att_list, att_signatures, + false, out_attestations, out_signatures); } @@ -1311,7 +1426,7 @@ static lantern_client_error validator_build_block_collect_attestations( goto cleanup; } - if (lantern_state_select_block_parent(&client->state, out_parent_root) != 0) + if (lantern_state_select_block_parent(&client->state, &client->store, out_parent_root) != 0) { result = LANTERN_CLIENT_ERR_RUNTIME; goto cleanup; @@ -1322,6 +1437,7 @@ static lantern_client_error validator_build_block_collect_attestations( LanternCheckpoint source_cp; if (lantern_state_compute_vote_checkpoints( &client->state, + &client->store, &head_cp, &target_cp, &source_cp) @@ -1345,6 +1461,7 @@ static lantern_client_error validator_build_block_collect_attestations( if (lantern_state_collect_attestations_for_block( &client->state, + &client->store, slot, local->global_index, out_parent_root, @@ -1409,6 +1526,7 @@ static lantern_client_error validator_build_block_populate_message( client, att_list, att_signatures, + false, &aggregated_attestations, &aggregated_signatures); if (agg_rc != LANTERN_CLIENT_OK) @@ -1486,7 +1604,7 @@ static lantern_client_error validator_build_block_preview_state_root( } lantern_client_error result = LANTERN_CLIENT_OK; - if (lantern_state_preview_post_state_root(&client->state, block, out_state_root) != 0) + if (lantern_state_preview_post_state_root(&client->state, &client->store, block, out_state_root) != 0) { result = LANTERN_CLIENT_ERR_RUNTIME; } @@ -1595,8 +1713,8 @@ int validator_store_vote(struct lantern_client *client, const LanternSignedVote { return LANTERN_CLIENT_ERR_RUNTIME; } - int rc = lantern_state_set_signed_validator_vote( - &client->state, + int rc = lantern_store_set_signed_validator_vote( + &client->store, (size_t)vote->data.validator_id, vote); lantern_client_unlock_state(client, state_locked); @@ -1615,7 +1733,7 @@ int validator_store_vote(struct lantern_client *client, const LanternSignedVote /** - * Publish a vote to the network. + * Publish a vote to the network and cache it for staged aggregation. * * @param client Client instance * @param vote Vote to publish @@ -1632,18 +1750,37 @@ int validator_publish_vote(struct lantern_client *client, const LanternSignedVot return LANTERN_CLIENT_ERR_INVALID_PARAM; } struct lantern_log_metadata meta = {.validator = client->node_id}; - if (client->has_fork_choice) + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) + { + return LANTERN_CLIENT_ERR_RUNTIME; + } + + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote->data.data, &data_root) == 0) { - if (lantern_fork_choice_add_vote(&client->fork_choice, vote, false) != 0) + LanternSignatureKey key = { + .validator_index = vote->data.validator_id, + .data_root = data_root, + }; + if (lantern_client_set_gossip_signature( + client, + &key, + &vote->data.data, + &vote->signature, + vote->data.target.slot) + != 0) { lantern_log_debug( "validator", &meta, - "failed to enqueue vote into fork choice validator=%" PRIu64 " slot=%" PRIu64, + "failed to cache local vote for aggregation validator=%" PRIu64 " slot=%" PRIu64, vote->data.validator_id, vote->data.slot); } } + lantern_client_unlock_state(client, state_locked); + int rc = lantern_gossipsub_service_publish_vote(&client->gossip, vote); if (rc != 0) { @@ -1892,26 +2029,26 @@ int validator_propose_block(struct lantern_client *client, uint64_t slot, size_t block.message.block.proposer_index); lantern_client_record_block(client, &block, NULL, NULL, "local", 0, false, NULL, 0); - rc = lantern_client_publish_block(client, &block); - if (rc != LANTERN_CLIENT_OK) - { - lantern_signed_block_with_attestation_reset(&block); - return rc; - } - if (client->validator_lock_initialized && pthread_mutex_lock(&client->validator_lock) == 0) { if (local_index < client->local_validator_count) { struct lantern_local_validator *local = &client->local_validators[local_index]; local->last_proposed_slot = slot; - local->pending_attestation = proposer_vote; - local->pending_attestation_slot = slot; - local->has_pending_attestation = true; + /* The proposer already attested inside the block wrapper at interval 0. */ + local->last_attested_slot = slot; + validator_clear_pending_attestation(local); } unlock_mutex_with_log(&client->validator_lock, client->node_id, "validator_lock"); } + rc = lantern_client_publish_block(client, &block); + if (rc != LANTERN_CLIENT_OK) + { + lantern_signed_block_with_attestation_reset(&block); + return rc; + } + lantern_signed_block_with_attestation_reset(&block); return LANTERN_CLIENT_OK; } @@ -1937,6 +2074,7 @@ int validator_propose_block(struct lantern_client *client, uint64_t slot, size_t int validator_publish_attestations(struct lantern_client *client, uint64_t slot) { lantern_client_error result = LANTERN_CLIENT_OK; + size_t num_validators = 0; if (!validator_service_should_run(client)) { @@ -1955,8 +2093,10 @@ int validator_publish_attestations(struct lantern_client *client, uint64_t slot) { return LANTERN_CLIENT_ERR_RUNTIME; } + num_validators = client->state.config.num_validators; if (lantern_state_compute_vote_checkpoints( &client->state, + &client->store, &head_cp, &target_cp, &source_cp) @@ -1985,6 +2125,16 @@ int validator_publish_attestations(struct lantern_client *client, uint64_t slot) continue; } struct lantern_local_validator *validator = &client->local_validators[i]; + if (lantern_validator_index_is_proposer_for( + (LanternValidatorIndex)validator->global_index, + slot, + num_validators)) + { + /* Proposers do not re-attest at interval 1. */ + validator->last_attested_slot = slot; + validator_clear_pending_attestation(validator); + continue; + } if (validator->last_attested_slot == slot) { continue; @@ -2013,7 +2163,7 @@ int validator_publish_attestations(struct lantern_client *client, uint64_t slot) } } validator->last_attested_slot = slot; - validator->has_pending_attestation = false; + validator_clear_pending_attestation(validator); int store_rc = validator_store_vote(client, &vote); if (store_rc != LANTERN_CLIENT_OK && result == LANTERN_CLIENT_OK) @@ -2054,24 +2204,26 @@ static lantern_client_error collect_subnet_votes_for_slot( if (!state_locked) { return LANTERN_CLIENT_ERR_RUNTIME; } - if (!client->has_state || !client->state.validator_votes || client->state.validator_votes_len == 0) { + if (!client->has_state) { lantern_client_unlock_state(client, state_locked); return LANTERN_CLIENT_ERR_RUNTIME; } lantern_client_error result = LANTERN_CLIENT_OK; - for (size_t i = 0; i < client->state.validator_votes_len; ++i) { - LanternSignedVote vote; - memset(&vote, 0, sizeof(vote)); - if (lantern_state_get_signed_validator_vote(&client->state, i, &vote) != 0) { + const struct lantern_gossip_signature_map *map = &client->store.gossip_signatures; + for (size_t i = 0; i < map->length; ++i) { + const struct lantern_gossip_signature_entry *entry = &map->entries[i]; + LanternAttestationData data; + memset(&data, 0, sizeof(data)); + if (lantern_store_get_attestation_data(&client->store, &entry->key.data_root, &data) != 0) { continue; } - if (vote.data.slot != slot) { + if (data.slot != slot) { continue; } size_t vote_subnet = 0; if (lantern_validator_index_compute_subnet_id( - vote.data.validator_id, + entry->key.validator_index, validator_attestation_committee_count(client), &vote_subnet) != 0) { @@ -2080,8 +2232,12 @@ static lantern_client_error collect_subnet_votes_for_slot( if (vote_subnet != subnet_id) { continue; } - if (lantern_attestations_append(out_attestations, &vote.data) != 0 - || lantern_signature_list_append(out_signatures, &vote.signature) != 0) { + LanternVote vote; + memset(&vote, 0, sizeof(vote)); + vote.validator_id = entry->key.validator_index; + vote.data = data; + if (lantern_attestations_append(out_attestations, &vote) != 0 + || lantern_signature_list_append(out_signatures, &entry->signature) != 0) { result = LANTERN_CLIENT_ERR_ALLOC; break; } @@ -2130,6 +2286,7 @@ static int validator_publish_aggregated_attestations(struct lantern_client *clie client, &attestations, &signatures, + true, &aggregated_attestations, &aggregated_signatures); uint64_t aggregation_finished_ms = monotonic_millis(); @@ -2178,9 +2335,10 @@ static int validator_publish_aggregated_attestations(struct lantern_client *clie if (lantern_hash_tree_root_attestation_data(&signed_attestation.data, &data_root) == 0) { bool locked = lantern_client_lock_state(client); if (locked) { - (void)lantern_client_agg_proof_cache_add( + (void)lantern_client_add_new_aggregated_payload( client, &data_root, + &signed_attestation.data, &signed_attestation.proof, signed_attestation.data.target.slot); } @@ -2198,6 +2356,160 @@ static int validator_publish_aggregated_attestations(struct lantern_client *clie } +int lantern_client_chain_service_tick_to( + struct lantern_client *client, + uint64_t target_interval, + bool has_proposal, + uint64_t *out_skipped_to_interval, + uint64_t *out_ticked_intervals) +{ + if (out_skipped_to_interval) { + *out_skipped_to_interval = UINT64_MAX; + } + if (out_ticked_intervals) { + *out_ticked_intervals = 0u; + } + if (!client || !client->has_fork_choice) { + return -1; + } + + bool skipped = false; + for (;;) + { + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) { + return -1; + } + if (!client->has_fork_choice) { + lantern_client_unlock_state(client, state_locked); + return -1; + } + + uint64_t current_interval = client->fork_choice.time_intervals; + if (current_interval >= target_interval) { + lantern_client_unlock_state(client, state_locked); + return 0; + } + + if (!skipped + && client->fork_choice.intervals_per_slot > 0 + && (target_interval - current_interval) > client->fork_choice.intervals_per_slot) + { + uint64_t skip_to_interval = + target_interval - (uint64_t)client->fork_choice.intervals_per_slot; + if (lantern_client_skip_fork_choice_intervals_locked(client, skip_to_interval) != 0) { + lantern_client_unlock_state(client, state_locked); + return -1; + } + if (out_skipped_to_interval) { + *out_skipped_to_interval = skip_to_interval; + } + skipped = true; + lantern_client_unlock_state(client, state_locked); + continue; + } + + if (lantern_client_tick_fork_choice_interval_locked(client, has_proposal) != 0) { + lantern_client_unlock_state(client, state_locked); + return -1; + } + + uint64_t advanced_interval = client->fork_choice.time_intervals; + lantern_client_unlock_state(client, state_locked); + + if (out_ticked_intervals) { + *out_ticked_intervals += 1u; + } + if (advanced_interval >= target_interval) { + return 0; + } + + timing_service_yield(); + } +} + + +/* ============================================================================ + * Timing Service Thread + * ============================================================================ */ + +void *timing_thread(void *arg) +{ + struct lantern_client *client = arg; + if (!client) { + return NULL; + } + + while (__atomic_load_n(&client->timing_stop_flag, __ATOMIC_RELAXED) == 0) + { + if (!timing_service_should_run(client)) + { + validator_sleep_ms(TIMING_SERVICE_IDLE_SLEEP_MS); + continue; + } + + uint64_t now_milliseconds = validator_wall_time_now_millis(); + uint64_t target_interval = 0u; + uint64_t genesis_milliseconds = 0u; + int target_rc = timing_service_compute_target_interval( + client, + now_milliseconds, + &target_interval, + &genesis_milliseconds); + if (target_rc > 0) + { + uint64_t sleep_milliseconds = TIMING_SERVICE_IDLE_SLEEP_MS; + if (genesis_milliseconds > now_milliseconds) { + sleep_milliseconds = genesis_milliseconds - now_milliseconds; + if (sleep_milliseconds > TIMING_SERVICE_IDLE_SLEEP_MS) { + sleep_milliseconds = TIMING_SERVICE_IDLE_SLEEP_MS; + } + } + validator_sleep_ms((uint32_t)sleep_milliseconds); + continue; + } + if (target_rc != 0) + { + validator_sleep_ms(TIMING_SERVICE_POLL_SLEEP_MS); + continue; + } + + uint64_t current_interval = 0u; + bool state_locked = lantern_client_lock_state(client); + if (!state_locked) + { + validator_sleep_ms(TIMING_SERVICE_POLL_SLEEP_MS); + continue; + } + current_interval = client->has_fork_choice ? client->fork_choice.time_intervals : 0u; + lantern_client_unlock_state(client, state_locked); + + if (current_interval >= target_interval) + { + uint32_t sleep_milliseconds = timing_service_sleep_until_next_interval( + client, + now_milliseconds, + target_interval); + if (sleep_milliseconds == 0u) { + timing_service_yield(); + } else { + validator_sleep_ms(sleep_milliseconds); + } + continue; + } + + bool has_proposal = client->validator_duty.pending_local_proposal + && !client->validator_duty.slot_proposed; + if (lantern_client_chain_service_tick_to(client, target_interval, has_proposal, NULL, NULL) != 0) + { + validator_sleep_ms(TIMING_SERVICE_POLL_SLEEP_MS); + } + } + + return NULL; +} + + /* ============================================================================ * Validator Service Thread * ============================================================================ */ @@ -2272,12 +2584,6 @@ void *validator_thread(void *arg) } duty->last_interval = tp->interval_index; - if (client->has_fork_choice) - { - bool has_proposal = duty->slot_proposed; - (void)lantern_fork_choice_advance_time(&client->fork_choice, now, has_proposal); - } - switch (tp->phase) { case LANTERN_DUTY_PHASE_PROPOSAL: @@ -2334,6 +2640,89 @@ void *validator_thread(void *arg) } +/** + * Start the timing service. + * + * @param client Client instance + * + * @return LANTERN_CLIENT_OK on success, or if service is already running or + * prerequisites are missing + * @return LANTERN_CLIENT_ERR_INVALID_PARAM if client is NULL + * @return LANTERN_CLIENT_ERR_RUNTIME if the thread cannot be created + * + * @note Thread safety: This function is thread-safe + */ +int start_timing_service(struct lantern_client *client) +{ + if (!client) + { + return LANTERN_CLIENT_ERR_INVALID_PARAM; + } + if (client->timing_thread_started) + { + return LANTERN_CLIENT_OK; + } + if (!client->has_runtime || !client->has_fork_choice) + { + return LANTERN_CLIENT_OK; + } + + __atomic_store_n(&client->timing_stop_flag, 0, __ATOMIC_RELAXED); + if (pthread_create(&client->timing_thread, NULL, timing_thread, client) != 0) + { + __atomic_store_n(&client->timing_stop_flag, 1, __ATOMIC_RELAXED); + lantern_log_warn( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "failed to start timing service thread"); + return LANTERN_CLIENT_ERR_RUNTIME; + } + + client->timing_thread_started = true; + lantern_log_info( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "fork-choice timing service started"); + return LANTERN_CLIENT_OK; +} + + +/** + * Stop the timing service. + * + * Signals the timing service thread to stop and waits for it to exit. + * Safe to call even if the service was never started. + * + * @param client Client instance (may be NULL) + * + * @note Thread safety: This function is thread-safe + */ +void stop_timing_service(struct lantern_client *client) +{ + if (!client || !client->timing_thread_started) + { + return; + } + + __atomic_store_n(&client->timing_stop_flag, 1, __ATOMIC_RELAXED); + int join_rc = pthread_join(client->timing_thread, NULL); + if (join_rc != 0) + { + lantern_log_warn( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "pthread_join failed: %d", + join_rc); + } + + client->timing_thread_started = false; + lantern_log_info( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "fork-choice timing service stopped"); +} + + /** * Start the validator service. * diff --git a/src/storage/storage.c b/src/storage/storage.c index 0354c2c..c1ce1ec 100644 --- a/src/storage/storage.c +++ b/src/storage/storage.c @@ -35,12 +35,9 @@ #define LANTERN_STORAGE_SLOT_INDEX_DIR "slots" #define LANTERN_STORAGE_STATE_FILE "state.ssz" #define LANTERN_STORAGE_FINALIZED_STATE_FILE "finalized_state.ssz" -#define LANTERN_STORAGE_STATE_META_FILE "state.meta" -#define LANTERN_STORAGE_FINALIZED_STATE_META_FILE "finalized_state.meta" #define LANTERN_STORAGE_VOTES_FILE "votes.bin" #define LANTERN_STORAGE_HEAD_FILE "head.bin" #define LANTERN_STORAGE_CHECKPOINTS_FILE "checkpoints.bin" -#define LANTERN_STORAGE_STATE_META_VERSION 1u #if defined(_WIN32) #define LANTERN_STORAGE_PATH_SEP '\\' @@ -56,13 +53,6 @@ struct lantern_storage_votes_header { uint64_t record_count; }; -struct lantern_storage_state_meta { - uint32_t version; - uint32_t reserved; - uint64_t historical_roots_offset; - uint64_t justified_slots_offset; -}; - struct lantern_storage_head_record { uint64_t slot; LanternRoot root; @@ -441,92 +431,6 @@ static int read_file_buffer(const char *path, uint8_t **out_data, size_t *out_le return rc; } -static int write_state_meta_leaf(const char *data_dir, const char *meta_leaf, const LanternState *state) { - if (!data_dir || !meta_leaf || !state) { - return -1; - } - int rc = -1; - char *meta_path = NULL; - - const struct lantern_storage_state_meta meta = { - .version = LANTERN_STORAGE_STATE_META_VERSION, - .reserved = 0, - .historical_roots_offset = state->historical_roots_offset, - .justified_slots_offset = state->justified_slots_offset, - }; - if (join_path(data_dir, meta_leaf, &meta_path) != 0) { - goto cleanup; - } - rc = write_atomic_file(meta_path, (const uint8_t *)&meta, sizeof(meta)); - -cleanup: - free_path(meta_path); - return rc; -} - -static int write_state_meta(const char *data_dir, const LanternState *state) { - return write_state_meta_leaf(data_dir, LANTERN_STORAGE_STATE_META_FILE, state); -} - -static int write_finalized_state_meta(const char *data_dir, const LanternState *state) { - return write_state_meta_leaf(data_dir, LANTERN_STORAGE_FINALIZED_STATE_META_FILE, state); -} - -static int write_state_meta_path(const char *path, const LanternState *state) { - if (!path || !state) { - return -1; - } - struct lantern_storage_state_meta meta = { - .version = LANTERN_STORAGE_STATE_META_VERSION, - .reserved = 0, - .historical_roots_offset = state->historical_roots_offset, - .justified_slots_offset = state->justified_slots_offset, - }; - return write_atomic_file(path, (const uint8_t *)&meta, sizeof(meta)); -} - -static int read_state_meta_leaf(const char *data_dir, const char *meta_leaf, struct lantern_storage_state_meta *meta) { - if (!data_dir || !meta_leaf || !meta) { - return -1; - } - int rc = -1; - char *meta_path = NULL; - uint8_t *buffer = NULL; - size_t len = 0; - - if (join_path(data_dir, meta_leaf, &meta_path) != 0) { - goto cleanup; - } - rc = read_file_buffer(meta_path, &buffer, &len); - if (rc != 0) { - goto cleanup; - } - if (len != sizeof(*meta)) { - rc = -1; - goto cleanup; - } - memcpy(meta, buffer, sizeof(*meta)); - if (meta->version != LANTERN_STORAGE_STATE_META_VERSION) { - rc = -1; - goto cleanup; - } - - rc = 0; - -cleanup: - free_path(meta_path); - free(buffer); - return rc; -} - -static int read_state_meta(const char *data_dir, struct lantern_storage_state_meta *meta) { - return read_state_meta_leaf(data_dir, LANTERN_STORAGE_STATE_META_FILE, meta); -} - -static int read_finalized_state_meta(const char *data_dir, struct lantern_storage_state_meta *meta) { - return read_state_meta_leaf(data_dir, LANTERN_STORAGE_FINALIZED_STATE_META_FILE, meta); -} - static int build_blocks_dir(const char *data_dir, char **out_path) { return join_path(data_dir, LANTERN_STORAGE_BLOCKS_DIR, out_path); } @@ -618,7 +522,7 @@ int lantern_storage_prepare(const char *data_dir) { } /** - * Persist `state` under `data_dir` using SSZ (`state.ssz`) plus `state.meta`. + * Persist `state` under `data_dir` using SSZ (`state.ssz`). * * @param data_dir Base directory path. * @param state State to persist. @@ -654,7 +558,7 @@ int lantern_storage_save_state(const char *data_dir, const LanternState *state) goto cleanup; } - rc = write_state_meta(data_dir, state); + rc = 0; cleanup: free_path(state_path); @@ -704,24 +608,6 @@ int lantern_storage_load_state(const char *data_dir, LanternState *state) { rc = -1; goto cleanup; } - if (lantern_state_prepare_validator_votes(&decoded, decoded.config.num_validators) != 0) { - rc = -1; - goto cleanup; - } - struct lantern_storage_state_meta meta; - const int meta_rc = read_state_meta(data_dir, &meta); - if (meta_rc == 0) { - decoded.historical_roots_offset = meta.historical_roots_offset; - decoded.justified_slots_offset = meta.justified_slots_offset; - } else if (meta_rc == 1) { - decoded.historical_roots_offset = 0; - decoded.justified_slots_offset = - decoded.latest_finalized.slot == UINT64_MAX ? 0u : (decoded.latest_finalized.slot + 1u); - } else { - rc = -1; - goto cleanup; - } - lantern_state_reset(state); *state = decoded; decoded_owned = false; @@ -737,7 +623,7 @@ int lantern_storage_load_state(const char *data_dir, LanternState *state) { } /** - * Persist finalized replay state under `data_dir/finalized_state.ssz` plus `finalized_state.meta`. + * Persist finalized replay state under `data_dir/finalized_state.ssz`. * * @param data_dir Base directory path. * @param state State to persist. @@ -773,7 +659,7 @@ int lantern_storage_save_finalized_state(const char *data_dir, const LanternStat goto cleanup; } - rc = write_finalized_state_meta(data_dir, state); + rc = 0; cleanup: free_path(state_path); @@ -823,24 +709,6 @@ int lantern_storage_load_finalized_state(const char *data_dir, LanternState *sta rc = -1; goto cleanup; } - if (lantern_state_prepare_validator_votes(&decoded, decoded.config.num_validators) != 0) { - rc = -1; - goto cleanup; - } - struct lantern_storage_state_meta meta; - const int meta_rc = read_finalized_state_meta(data_dir, &meta); - if (meta_rc == 0) { - decoded.historical_roots_offset = meta.historical_roots_offset; - decoded.justified_slots_offset = meta.justified_slots_offset; - } else if (meta_rc == 1) { - decoded.historical_roots_offset = 0; - decoded.justified_slots_offset = - decoded.latest_finalized.slot == UINT64_MAX ? 0u : (decoded.latest_finalized.slot + 1u); - } else { - rc = -1; - goto cleanup; - } - lantern_state_reset(state); *state = decoded; decoded_owned = false; @@ -863,8 +731,11 @@ int lantern_storage_load_finalized_state(const char *data_dir, LanternState *sta * @return 0 on success. * @return -1 on invalid parameters, encoding failure, or filesystem errors. */ -int lantern_storage_save_votes(const char *data_dir, const LanternState *state) { - if (!data_dir || !state || state->config.num_validators == 0) { +int lantern_storage_save_votes( + const char *data_dir, + const LanternState *state, + const LanternStore *store) { + if (!data_dir || !state || !store || state->config.num_validators == 0) { return -1; } @@ -872,13 +743,13 @@ int lantern_storage_save_votes(const char *data_dir, const LanternState *state) uint8_t *buffer = NULL; char *votes_path = NULL; - const size_t capacity = lantern_state_validator_capacity(state); + const size_t capacity = lantern_store_validator_capacity(store); if (capacity == 0) { goto cleanup; } size_t present = 0; for (size_t i = 0; i < capacity; ++i) { - if (lantern_state_validator_has_vote(state, i)) { + if (lantern_store_validator_has_vote(store, i)) { present++; } } @@ -899,7 +770,7 @@ int lantern_storage_save_votes(const char *data_dir, const LanternState *state) cursor += sizeof(header); for (size_t i = 0; i < capacity; ++i) { - if (!lantern_state_validator_has_vote(state, i)) { + if (!lantern_store_validator_has_vote(store, i)) { continue; } const uint64_t validator_index = (uint64_t)i; @@ -909,7 +780,7 @@ int lantern_storage_save_votes(const char *data_dir, const LanternState *state) cursor += sizeof(validator_index); LanternSignedVote signed_vote; - if (lantern_state_get_signed_validator_vote(state, i, &signed_vote) != 0) { + if (lantern_store_get_signed_validator_vote(store, i, &signed_vote) != 0) { goto cleanup; } size_t vote_written = 0; @@ -945,8 +816,8 @@ int lantern_storage_save_votes(const char *data_dir, const LanternState *state) * @return 1 if the votes file is missing or empty. * @return -1 on invalid parameters or decode/validation errors. */ -int lantern_storage_load_votes(const char *data_dir, LanternState *state) { - if (!data_dir || !state) { +int lantern_storage_load_votes(const char *data_dir, LanternState *state, LanternStore *store) { + if (!data_dir || !state || !store) { return -1; } @@ -994,13 +865,13 @@ int lantern_storage_load_votes(const char *data_dir, LanternState *state) { rc = -1; goto cleanup; } - if (lantern_state_prepare_validator_votes(state, state->config.num_validators) != 0) { + if (lantern_store_prepare_validator_votes(store, state->config.num_validators) != 0) { rc = -1; goto cleanup; } - const size_t capacity = lantern_state_validator_capacity(state); + const size_t capacity = lantern_store_validator_capacity(store); for (size_t i = 0; i < capacity; ++i) { - lantern_state_clear_validator_vote(state, i); + lantern_store_clear_validator_vote(store, i); } const uint8_t *cursor = data + sizeof(header); @@ -1018,7 +889,7 @@ int lantern_storage_load_votes(const char *data_dir, LanternState *state) { } cursor += sizeof(validator_index); remaining -= sizeof(validator_index); - if (validator_index >= state->validator_votes_len) { + if (validator_index >= store->validator_votes_len) { rc = -1; goto cleanup; } @@ -1031,7 +902,7 @@ int lantern_storage_load_votes(const char *data_dir, LanternState *state) { } cursor += signed_vote_size; remaining -= signed_vote_size; - if (lantern_state_set_signed_validator_vote(state, (size_t)validator_index, &signed_vote) != 0) { + if (lantern_store_set_signed_validator_vote(store, (size_t)validator_index, &signed_vote) != 0) { rc = -1; goto cleanup; } @@ -1044,7 +915,7 @@ int lantern_storage_load_votes(const char *data_dir, LanternState *state) { } cursor += LANTERN_VOTE_SSZ_SIZE; remaining -= LANTERN_VOTE_SSZ_SIZE; - if (lantern_state_set_validator_vote(state, (size_t)validator_index, &vote) != 0) { + if (lantern_store_set_validator_vote(store, (size_t)validator_index, &vote) != 0) { rc = -1; goto cleanup; } @@ -1266,7 +1137,6 @@ int lantern_storage_store_state_for_root( uint8_t *buffer = NULL; char *states_dir = NULL; char *state_path = NULL; - char *meta_path = NULL; const size_t encoded_size = state_encoded_size(state); if (encoded_size == 0) { @@ -1302,21 +1172,9 @@ int lantern_storage_store_state_for_root( if (rc != 0) { goto cleanup; } - - char meta_name[sizeof(root_hex) + 6]; - const int meta_written = snprintf(meta_name, sizeof(meta_name), "%s.meta", root_hex); - if (meta_written < 0 || (size_t)meta_written >= sizeof(meta_name)) { - rc = -1; - goto cleanup; - } - if (join_path(states_dir, meta_name, &meta_path) != 0) { - rc = -1; - goto cleanup; - } - rc = write_state_meta_path(meta_path, state); + rc = 0; cleanup: - free_path(meta_path); free_path(state_path); free_path(states_dir); free(buffer); diff --git a/tests/integration/consensus_fixture_runner.c b/tests/integration/consensus_fixture_runner.c index f9ea3c1..713d374 100644 --- a/tests/integration/consensus_fixture_runner.c +++ b/tests/integration/consensus_fixture_runner.c @@ -7,6 +7,7 @@ #include "lantern/support/strings.h" #include "fixture_runner.h" #include "tests/support/fixture_loader.h" +#include "../support/state_store_adapter.h" #include #include @@ -51,6 +52,84 @@ static bool is_root_zero(const LanternRoot *root) { return true; } +static int preview_post_state_root_without_signatures( + const LanternState *state, + const LanternSignedBlock *signed_block, + LanternRoot *out_state_root) { + if (!state || !signed_block || !out_state_root) { + return -1; + } + if (signed_block->message.block.slot <= state->slot) { + return -1; + } + + LanternState scratch; + lantern_state_init(&scratch); + if (lantern_state_clone(state, &scratch) != 0) { + return -1; + } + + int rc = 0; + if (lantern_state_process_slots(&scratch, signed_block->message.block.slot) != 0) { + rc = -1; + goto cleanup; + } + + LanternSignedVote proposer_signed; + memset(&proposer_signed, 0, sizeof(proposer_signed)); + proposer_signed.data = signed_block->message.proposer_attestation; + proposer_signed.signature = signed_block->signatures.proposer_signature; + if (lantern_state_process_block( + &scratch, + &signed_block->message.block, + NULL, + &proposer_signed) + != 0) { + rc = -1; + goto cleanup; + } + if (lantern_hash_tree_root_state(&scratch, out_state_root) != 0) { + rc = -1; + } + +cleanup: + lantern_state_reset(&scratch); + return rc; +} + +static int state_transition_without_signatures( + LanternState *state, + const LanternSignedBlock *signed_block) { + if (!state || !signed_block) { + return -1; + } + + const LanternBlock *block = &signed_block->message.block; + if (block->slot <= state->slot) { + return -1; + } + if (lantern_state_process_slots(state, block->slot) != 0) { + return -1; + } + + LanternSignedVote proposer_signed; + memset(&proposer_signed, 0, sizeof(proposer_signed)); + proposer_signed.data = signed_block->message.proposer_attestation; + proposer_signed.signature = signed_block->signatures.proposer_signature; + if (lantern_state_process_block(state, block, NULL, &proposer_signed) != 0) { + return -1; + } + + LanternRoot computed_state_root; + if (lantern_hash_tree_root_state(state, &computed_state_root) != 0) { + return -1; + } + if (memcmp(block->state_root.bytes, computed_state_root.bytes, LANTERN_ROOT_SIZE) != 0) { + return -1; + } + return 0; +} + /* Patch block hashes to use C-computed values instead of LeanSpec-computed values. * LeanSpec uses variable-length XMSS signatures which produces different hashes. * This function computes the correct parent_root and state_root using C code. @@ -80,9 +159,11 @@ static int patch_block_hashes_for_c_compat( } memcpy(signed_block->message.block.parent_root.bytes, computed_parent.bytes, LANTERN_ROOT_SIZE); - /* Now compute state_root using preview function (which internally checks parent_root) */ + /* The state_transition fixtures do not ship signature material. Preview using + * the unsigned transition path that mirrors leanSpec's valid_signatures + * precondition instead of the strict signed-block API. */ LanternRoot computed_state_root; - if (lantern_state_preview_post_state_root(state, signed_block, &computed_state_root) != 0) { + if (preview_post_state_root_without_signatures(state, signed_block, &computed_state_root) != 0) { return -1; } @@ -869,7 +950,7 @@ static int run_state_transition_fixture(const char *path) { } } - int status = lantern_state_transition(&state, &signed_block); + int status = state_transition_without_signatures(&state, &signed_block); reset_block(&signed_block.message); if (status != 0) { @@ -1030,6 +1111,18 @@ static int run_fork_choice_fixture(const char *path) { LanternForkChoice store; lantern_fork_choice_init(&store); + lantern_state_attach_fork_choice(&state, &store); + if (lantern_store_prepare_fork_choice_votes( + lantern_test_state_store_ensure(&state), + validator_count) + != 0) { + reset_block(&anchor_block); + 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; + } LanternConfig config = { .num_validators = validator_count, .genesis_time = genesis_time, @@ -1064,8 +1157,6 @@ static int run_fork_choice_fixture(const char *path) { return -1; } - lantern_state_attach_fork_choice(&state, &store); - if (stored_state_save(&stored_states, &stored_states_count, &stored_states_cap, &anchor_root, &state) != 0) { reset_block(&anchor_block); lantern_fork_choice_reset(&store); @@ -1233,7 +1324,7 @@ static int run_fork_choice_fixture(const char *path) { return -1; } if (signed_block.message.slot > state.slot) { - if (lantern_state_transition(&state, &signed_block) != 0) { + if (state_transition_without_signatures(&state, &signed_block) != 0) { reset_block(&signed_block.message); reset_block(&anchor_block); lantern_fork_choice_reset(&store); @@ -1337,7 +1428,7 @@ static int run_fork_choice_fixture(const char *path) { hash_mapping_reset(&hash_mapping, &hash_mapping_count, &hash_mapping_cap); return -1; } - if (lantern_state_transition(active_state, &signed_block) != 0) { + if (state_transition_without_signatures(active_state, &signed_block) != 0) { lantern_state_reset(&branch_state); reset_block(&signed_block.message); reset_block(&anchor_block); diff --git a/tests/support/fixture_loader.c b/tests/support/fixture_loader.c index cbc03eb..eeff29e 100644 --- a/tests/support/fixture_loader.c +++ b/tests/support/fixture_loader.c @@ -10,6 +10,7 @@ #include "lantern/consensus/hash.h" #include "lantern/support/strings.h" #include "external/c-leanvm-xmss/include/pq-bindings-c-rust.h" +#include "state_store_adapter.h" #define JSON_INITIAL_TOKENS 256 #define LANTERN_XMSS_FP_BYTES 4u @@ -1746,8 +1747,6 @@ int lantern_fixture_parse_anchor_state( } state->latest_justified = *latest_justified; state->latest_finalized = *latest_finalized; - state->justified_slots_offset = - latest_finalized->slot == UINT64_MAX ? 0u : (latest_finalized->slot + 1u); int header_idx = lantern_fixture_object_get_field(doc, anchor_state_index, "latestBlockHeader"); if (header_idx < 0) { diff --git a/tests/support/state_store_adapter.c b/tests/support/state_store_adapter.c new file mode 100644 index 0000000..4c9c217 --- /dev/null +++ b/tests/support/state_store_adapter.c @@ -0,0 +1,53 @@ +#include "state_store_adapter.h" + +#include + +static struct lantern_test_state_store_slot g_lantern_test_state_store_slots[LANTERN_TEST_STATE_STORE_SLOT_CAP]; + +struct lantern_test_state_store_slot *lantern_test_state_store_slots(void) { + return g_lantern_test_state_store_slots; +} + +struct lantern_test_state_store_slot *lantern_test_state_store_find_slot(const LanternState *state) { + if (!state) { + return NULL; + } + struct lantern_test_state_store_slot *slots = lantern_test_state_store_slots(); + for (size_t i = 0; i < LANTERN_TEST_STATE_STORE_SLOT_CAP; ++i) { + if (slots[i].in_use && slots[i].state == state) { + return &slots[i]; + } + } + return NULL; +} + +LanternStore *lantern_test_state_store_ensure(LanternState *state) { + struct lantern_test_state_store_slot *slot = lantern_test_state_store_find_slot(state); + if (slot) { + return &slot->store; + } + struct lantern_test_state_store_slot *slots = lantern_test_state_store_slots(); + for (size_t i = 0; i < LANTERN_TEST_STATE_STORE_SLOT_CAP; ++i) { + if (!slots[i].in_use) { + slots[i].in_use = true; + slots[i].state = state; + lantern_store_init(&slots[i].store); + return &slots[i].store; + } + } + return NULL; +} + +const LanternStore *lantern_test_state_store_find(const LanternState *state) { + struct lantern_test_state_store_slot *slot = lantern_test_state_store_find_slot(state); + return slot ? &slot->store : NULL; +} + +void lantern_test_state_store_release(LanternState *state) { + struct lantern_test_state_store_slot *slot = lantern_test_state_store_find_slot(state); + if (!slot) { + return; + } + lantern_store_reset(&slot->store); + memset(slot, 0, sizeof(*slot)); +} diff --git a/tests/support/state_store_adapter.h b/tests/support/state_store_adapter.h new file mode 100644 index 0000000..49fbae0 --- /dev/null +++ b/tests/support/state_store_adapter.h @@ -0,0 +1,217 @@ +#ifndef LANTERN_TESTS_STATE_STORE_ADAPTER_H +#define LANTERN_TESTS_STATE_STORE_ADAPTER_H + +#include +#include +#include +#include + +#include "lantern/consensus/state.h" +#include "lantern/consensus/store.h" + +#define lantern_state_init_explicit lantern_state_init +#define lantern_state_reset_explicit lantern_state_reset +#define lantern_state_clone_explicit lantern_state_clone +#define lantern_state_generate_genesis_explicit lantern_state_generate_genesis +#define lantern_state_process_attestations_explicit lantern_state_process_attestations +#define lantern_state_process_block_explicit lantern_state_process_block +#define lantern_state_transition_explicit lantern_state_transition +#define lantern_state_select_block_parent_explicit lantern_state_select_block_parent +#define lantern_state_collect_attestations_for_block_explicit lantern_state_collect_attestations_for_block +#define lantern_state_compute_vote_checkpoints_explicit lantern_state_compute_vote_checkpoints +#define lantern_state_preview_post_state_root_explicit lantern_state_preview_post_state_root + +enum { LANTERN_TEST_STATE_STORE_SLOT_CAP = 64 }; + +struct lantern_test_state_store_slot { + LanternState *state; + LanternStore store; + bool in_use; +}; + +struct lantern_test_state_store_slot *lantern_test_state_store_slots(void); +struct lantern_test_state_store_slot *lantern_test_state_store_find_slot(const LanternState *state); +LanternStore *lantern_test_state_store_ensure(LanternState *state); +const LanternStore *lantern_test_state_store_find(const LanternState *state); +void lantern_test_state_store_release(LanternState *state); + +static inline void lantern_test_state_init(LanternState *state) { + lantern_test_state_store_release(state); + lantern_state_init_explicit(state); +} + +static inline void lantern_test_state_reset(LanternState *state) { + lantern_test_state_store_release(state); + lantern_state_reset_explicit(state); +} + +static inline int lantern_test_state_clone(const LanternState *source, LanternState *dest) { + int rc = lantern_state_clone_explicit(source, dest); + if (rc != 0) { + return rc; + } + const LanternStore *source_store = lantern_test_state_store_find(source); + if (!source_store) { + return 0; + } + LanternStore *dest_store = lantern_test_state_store_ensure(dest); + if (!dest_store) { + lantern_state_reset_explicit(dest); + return -1; + } + if (lantern_store_clone_validator_votes(source_store, dest_store) != 0) { + lantern_test_state_reset(dest); + return -1; + } + return 0; +} + +static inline int lantern_test_state_generate_genesis( + LanternState *state, + uint64_t genesis_time, + uint64_t num_validators) { + int rc = lantern_state_generate_genesis_explicit(state, genesis_time, num_validators); + if (rc != 0) { + return rc; + } + LanternStore *store = lantern_test_state_store_ensure(state); + if (!store) { + return -1; + } + return lantern_store_prepare_validator_votes(store, num_validators); +} + +static inline int lantern_test_state_prepare_validator_votes(LanternState *state, uint64_t validator_count) { + LanternStore *store = lantern_test_state_store_ensure(state); + return store ? lantern_store_prepare_validator_votes(store, validator_count) : -1; +} + +static inline size_t lantern_test_state_validator_capacity(const LanternState *state) { + const LanternStore *store = lantern_test_state_store_find(state); + return store ? lantern_store_validator_capacity(store) : 0u; +} + +static inline bool lantern_test_state_validator_has_vote(const LanternState *state, size_t index) { + const LanternStore *store = lantern_test_state_store_find(state); + return store ? lantern_store_validator_has_vote(store, index) : false; +} + +static inline int lantern_test_state_get_signed_validator_vote( + const LanternState *state, + size_t index, + LanternSignedVote *out_vote) { + const LanternStore *store = lantern_test_state_store_find(state); + return store ? lantern_store_get_signed_validator_vote(store, index, out_vote) : -1; +} + +static inline int lantern_test_state_get_validator_vote( + const LanternState *state, + size_t index, + LanternVote *out_vote) { + const LanternStore *store = lantern_test_state_store_find(state); + return store ? lantern_store_get_validator_vote(store, index, out_vote) : -1; +} + +static inline int lantern_test_state_set_signed_validator_vote( + LanternState *state, + size_t index, + const LanternSignedVote *vote) { + LanternStore *store = lantern_test_state_store_ensure(state); + return store ? lantern_store_set_signed_validator_vote(store, index, vote) : -1; +} + +static inline int lantern_test_state_set_validator_vote( + LanternState *state, + size_t index, + const LanternVote *vote) { + LanternStore *store = lantern_test_state_store_ensure(state); + return store ? lantern_store_set_validator_vote(store, index, vote) : -1; +} + +static inline void lantern_test_state_clear_validator_vote(LanternState *state, size_t index) { + LanternStore *store = lantern_test_state_store_ensure(state); + if (store) { + lantern_store_clear_validator_vote(store, index); + } +} + +static inline void lantern_test_state_attach_fork_choice( + LanternState *state, + struct lantern_fork_choice *fork_choice) { + LanternStore *store = lantern_test_state_store_ensure(state); + if (store) { + lantern_store_attach_fork_choice(store, fork_choice); + } +} + +#define lantern_state_init(state) lantern_test_state_init((state)) +#define lantern_state_reset(state) lantern_test_state_reset((state)) +#define lantern_state_clone(source, dest) lantern_test_state_clone((source), (dest)) +#define lantern_state_generate_genesis(state, genesis_time, num_validators) \ + lantern_test_state_generate_genesis((state), (genesis_time), (num_validators)) +#define lantern_state_prepare_validator_votes(state, validator_count) \ + lantern_test_state_prepare_validator_votes((state), (validator_count)) +#define lantern_state_validator_capacity(state) lantern_test_state_validator_capacity((state)) +#define lantern_state_validator_has_vote(state, index) \ + lantern_test_state_validator_has_vote((state), (index)) +#define lantern_state_get_signed_validator_vote(state, index, out_vote) \ + lantern_test_state_get_signed_validator_vote((state), (index), (out_vote)) +#define lantern_state_get_validator_vote(state, index, out_vote) \ + lantern_test_state_get_validator_vote((state), (index), (out_vote)) +#define lantern_state_set_signed_validator_vote(state, index, vote) \ + lantern_test_state_set_signed_validator_vote((state), (index), (vote)) +#define lantern_state_set_validator_vote(state, index, vote) \ + lantern_test_state_set_validator_vote((state), (index), (vote)) +#define lantern_state_clear_validator_vote(state, index) \ + lantern_test_state_clear_validator_vote((state), (index)) +#define lantern_state_attach_fork_choice(state, fork_choice) \ + lantern_test_state_attach_fork_choice((state), (fork_choice)) +#define lantern_state_process_attestations(state, attestations, signatures) \ + lantern_state_process_attestations_explicit( \ + (state), \ + lantern_test_state_store_ensure((state)), \ + (attestations), \ + (signatures)) +#define lantern_state_process_block(state, block, signatures, proposer_attestation) \ + lantern_state_process_block_explicit( \ + (state), \ + lantern_test_state_store_ensure((state)), \ + (block), \ + (signatures), \ + (proposer_attestation)) +#define lantern_state_transition(state, signed_block) \ + lantern_state_transition_explicit( \ + (state), \ + lantern_test_state_store_ensure((state)), \ + (signed_block)) +#define lantern_state_select_block_parent(state, out_parent_root) \ + lantern_state_select_block_parent_explicit( \ + (state), \ + lantern_test_state_store_ensure((state)), \ + (out_parent_root)) +#define lantern_state_collect_attestations_for_block( \ + state, block_slot, proposer_index, parent_root, proposer_attestation, out_attestations, out_signatures) \ + lantern_state_collect_attestations_for_block_explicit( \ + (state), \ + lantern_test_state_store_ensure((LanternState *)(state)), \ + (block_slot), \ + (proposer_index), \ + (parent_root), \ + (proposer_attestation), \ + (out_attestations), \ + (out_signatures)) +#define lantern_state_compute_vote_checkpoints(state, out_head, out_target, out_source) \ + lantern_state_compute_vote_checkpoints_explicit( \ + (state), \ + lantern_test_state_store_ensure((LanternState *)(state)), \ + (out_head), \ + (out_target), \ + (out_source)) +#define lantern_state_preview_post_state_root(state, block, out_state_root) \ + lantern_state_preview_post_state_root_explicit( \ + (state), \ + lantern_test_state_store_ensure((LanternState *)(state)), \ + (block), \ + (out_state_root)) + +#endif /* LANTERN_TESTS_STATE_STORE_ADAPTER_H */ diff --git a/tests/unit/client_test_helpers.c b/tests/unit/client_test_helpers.c index 685a959..884f518 100644 --- a/tests/unit/client_test_helpers.c +++ b/tests/unit/client_test_helpers.c @@ -10,6 +10,7 @@ #include #include "lantern/consensus/hash.h" +#include "lantern/consensus/duties.h" #include "lantern/consensus/signature.h" #include "lantern/crypto/xmss.h" #include "lantern/support/time.h" @@ -129,6 +130,7 @@ static void reset_vote_client_on_error(struct lantern_client *client) { lantern_fork_choice_reset(&client->fork_choice); client->has_fork_choice = false; } + lantern_store_reset(&client->store); if (client->has_state) { lantern_state_reset(&client->state); client->has_state = false; @@ -159,18 +161,22 @@ static int client_test_setup_vote_validation_client_common( uint8_t *serialized_pubkeys = NULL; LanternBlock anchor; LanternBlock child; + LanternSignedBlock child_signed; bool anchor_body_init = false; bool child_body_init = false; LanternRoot anchor_root_local; LanternRoot child_root_local; memset(&anchor_root_local, 0, sizeof(anchor_root_local)); memset(&child_root_local, 0, sizeof(child_root_local)); + memset(&child_signed, 0, sizeof(child_signed)); memset(client, 0, sizeof(*client)); client->node_id = (char *)node_id; client->debug_disable_fork_choice_time = true; + lantern_store_init(&client->store); lantern_state_init(&client->state); lantern_fork_choice_init(&client->fork_choice); + lantern_store_attach_fork_choice(&client->store, &client->fork_choice); if (pthread_mutex_init(&client->state_lock, NULL) != 0) { fprintf(stderr, "failed to initialize state mutex for vote test\n"); @@ -194,11 +200,68 @@ static int client_test_setup_vote_validation_client_common( } client->has_state = true; + if (lantern_store_prepare_validator_votes(&client->store, (uint64_t)validator_count) != 0 + || lantern_store_prepare_fork_choice_votes(&client->store, (uint64_t)validator_count) != 0) { + fprintf(stderr, "failed to prepare store caches for vote test\n"); + goto finish; + } + if (lantern_fork_choice_configure(&client->fork_choice, &client->state.config) != 0) { fprintf(stderr, "failed to configure fork choice for vote test\n"); goto finish; } + if (load_precomputed_keys(&pub, &secret) != 0) { + goto finish; + } + + uint8_t serialized_pub[LANTERN_VALIDATOR_PUBKEY_SIZE]; + uintptr_t written = 0; + enum PQSigningError serialize_err = + pq_public_key_serialize(pub, serialized_pub, sizeof(serialized_pub), &written); + if (serialize_err != Success || written == 0 || written > sizeof(serialized_pub)) { + fprintf( + stderr, + "failed to serialize pubkey for vote test (%d) needed=%zu\n", + (int)serialize_err, + (size_t)written); + goto finish; + } + if (written < sizeof(serialized_pub)) { + memset(serialized_pub + written, 0, sizeof(serialized_pub) - written); + } + + if (validator_count > (SIZE_MAX / LANTERN_VALIDATOR_PUBKEY_SIZE)) { + fprintf(stderr, "validator count too large for pubkey array\n"); + goto finish; + } + size_t total_pubkeys_len = validator_count * LANTERN_VALIDATOR_PUBKEY_SIZE; + serialized_pubkeys = calloc(total_pubkeys_len, 1u); + if (!serialized_pubkeys) { + fprintf(stderr, "failed to allocate validator pubkey array for vote test\n"); + goto finish; + } + for (size_t i = 0; i < validator_count; ++i) { + memcpy( + serialized_pubkeys + (i * LANTERN_VALIDATOR_PUBKEY_SIZE), + serialized_pub, + LANTERN_VALIDATOR_PUBKEY_SIZE); + } + + if (lantern_state_set_validator_pubkeys(&client->state, serialized_pubkeys, validator_count) != 0) { + fprintf(stderr, "failed to set validator pubkeys for vote test\n"); + goto finish; + } + const uint8_t *stored_pub = lantern_state_validator_pubkey(&client->state, 0); + if (!stored_pub) { + fprintf(stderr, "stored validator pubkey missing after load\n"); + goto finish; + } + if (memcmp(stored_pub, serialized_pub, LANTERN_VALIDATOR_PUBKEY_SIZE) != 0) { + fprintf(stderr, "stored validator pubkey mismatch after load\n"); + goto finish; + } + LanternRoot anchor_state_root; if (lantern_hash_tree_root_state(&client->state, &anchor_state_root) != 0) { fprintf(stderr, "failed to hash anchor state for vote test\n"); @@ -210,7 +273,10 @@ static int client_test_setup_vote_validation_client_common( lantern_block_body_init(&anchor.body); anchor_body_init = true; anchor.slot = 0; - anchor.proposer_index = 0; + if (lantern_proposer_for_slot(anchor.slot, client->state.config.num_validators, &anchor.proposer_index) != 0) { + fprintf(stderr, "failed to compute anchor proposer for vote test\n"); + goto finish; + } anchor.parent_root = client->state.latest_block_header.parent_root; anchor.state_root = anchor_state_root; @@ -241,9 +307,23 @@ static int client_test_setup_vote_validation_client_common( lantern_block_body_init(&child.body); child_body_init = true; child.slot = anchor.slot + 1u; - child.proposer_index = 0; + if (lantern_proposer_for_slot(child.slot, client->state.config.num_validators, &child.proposer_index) != 0) { + fprintf(stderr, "failed to compute child proposer for vote test\n"); + goto finish; + } child.parent_root = anchor_root_local; - client_test_fill_root(&child.state_root, 0xA1); + child_signed.message.block = child; + + if (lantern_state_preview_post_state_root( + &client->state, + &client->store, + &child_signed, + &child.state_root) + != 0) { + fprintf(stderr, "failed to preview child post-state root for vote test\n"); + goto finish; + } + child_signed.message.block = child; if (lantern_hash_tree_root_block(&child, &child_root_local) != 0) { fprintf(stderr, "failed to hash child block for vote test\n"); @@ -261,58 +341,14 @@ static int client_test_setup_vote_validation_client_common( fprintf(stderr, "failed to add child block for vote test\n"); goto finish; } - - lantern_state_attach_fork_choice(&client->state, &client->fork_choice); client->has_fork_choice = true; - if (load_precomputed_keys(&pub, &secret) != 0) { - goto finish; - } - - uint8_t serialized_pub[LANTERN_VALIDATOR_PUBKEY_SIZE]; - uintptr_t written = 0; - enum PQSigningError serialize_err = - pq_public_key_serialize(pub, serialized_pub, sizeof(serialized_pub), &written); - if (serialize_err != Success || written == 0 || written > sizeof(serialized_pub)) { - fprintf( - stderr, - "failed to serialize pubkey for vote test (%d) needed=%zu\n", - (int)serialize_err, - (size_t)written); - goto finish; - } - if (written < sizeof(serialized_pub)) { - memset(serialized_pub + written, 0, sizeof(serialized_pub) - written); - } - - if (validator_count > (SIZE_MAX / LANTERN_VALIDATOR_PUBKEY_SIZE)) { - fprintf(stderr, "validator count too large for pubkey array\n"); - goto finish; - } - size_t total_pubkeys_len = validator_count * LANTERN_VALIDATOR_PUBKEY_SIZE; - serialized_pubkeys = calloc(total_pubkeys_len, 1u); - if (!serialized_pubkeys) { - fprintf(stderr, "failed to allocate validator pubkey array for vote test\n"); + if (lantern_state_process_slots(&client->state, child.slot) != 0) { + fprintf(stderr, "failed to advance state slot for vote test child block\n"); goto finish; } - for (size_t i = 0; i < validator_count; ++i) { - memcpy( - serialized_pubkeys + (i * LANTERN_VALIDATOR_PUBKEY_SIZE), - serialized_pub, - LANTERN_VALIDATOR_PUBKEY_SIZE); - } - - if (lantern_state_set_validator_pubkeys(&client->state, serialized_pubkeys, validator_count) != 0) { - fprintf(stderr, "failed to set validator pubkeys for vote test\n"); - goto finish; - } - const uint8_t *stored_pub = lantern_state_validator_pubkey(&client->state, 0); - if (!stored_pub) { - fprintf(stderr, "stored validator pubkey missing after load\n"); - goto finish; - } - if (memcmp(stored_pub, serialized_pub, LANTERN_VALIDATOR_PUBKEY_SIZE) != 0) { - fprintf(stderr, "stored validator pubkey mismatch after load\n"); + if (lantern_state_process_block(&client->state, &client->store, &child, NULL, NULL) != 0) { + fprintf(stderr, "failed to process child block into vote test state\n"); goto finish; } diff --git a/tests/unit/test_client_gossip.c b/tests/unit/test_client_gossip.c index eed9b4f..11e08a3 100644 --- a/tests/unit/test_client_gossip.c +++ b/tests/unit/test_client_gossip.c @@ -9,16 +9,10 @@ static void reset_agg_cache(struct lantern_client *client) { - if (!client || !client->agg_proof_cache.entries) { + if (!client) { return; } - for (size_t i = 0; i < client->agg_proof_cache.length; ++i) { - lantern_aggregated_signature_proof_reset(&client->agg_proof_cache.entries[i].proof); - } - free(client->agg_proof_cache.entries); - client->agg_proof_cache.entries = NULL; - client->agg_proof_cache.length = 0; - client->agg_proof_cache.capacity = 0; + lantern_store_reset(&client->store); } static int build_single_participant_aggregated_attestation( @@ -182,10 +176,21 @@ static int test_gossip_aggregated_attestation_caches_valid_proof(void) fprintf(stderr, "valid aggregated attestation gossip should be accepted rc=%d\n", gossip_rc); goto cleanup_attestation; } - if (client.agg_proof_cache.length != 1 || !client.agg_proof_cache.entries) { + if (client.store.new_aggregated_payloads.length != 1 + || !client.store.new_aggregated_payloads.entries) { fprintf(stderr, "valid aggregated attestation should be cached\n"); goto cleanup_attestation; } + if (client.store.known_aggregated_payloads.length != 0) { + fprintf(stderr, "valid aggregated attestation should remain pending until migration\n"); + goto cleanup_attestation; + } + if (client.fork_choice.new_aggregated_payloads != &client.store.new_aggregated_payloads + || client.fork_choice.known_aggregated_payloads != &client.store.known_aggregated_payloads + || client.fork_choice.attestation_data_by_root != &client.store.attestation_data_by_root) { + fprintf(stderr, "fork choice should expose attached aggregated attestation store views\n"); + goto cleanup_attestation; + } LanternRoot data_root; if (lantern_hash_tree_root_attestation_data(&attestation.data, &data_root) != 0) { @@ -193,17 +198,50 @@ static int test_gossip_aggregated_attestation_caches_valid_proof(void) goto cleanup_attestation; } if (memcmp( - client.agg_proof_cache.entries[0].data_root.bytes, + client.store.new_aggregated_payloads.entries[0].data_root.bytes, data_root.bytes, LANTERN_ROOT_SIZE) != 0) { fprintf(stderr, "cached aggregated proof root mismatch\n"); goto cleanup_attestation; } - if (client.agg_proof_cache.entries[0].target_slot != attestation.data.target.slot) { + if (client.store.new_aggregated_payloads.entries[0].target_slot != attestation.data.target.slot) { fprintf(stderr, "cached aggregated proof target slot mismatch\n"); goto cleanup_attestation; } + if (!client.fork_choice.new_aggregated_payloads + || client.fork_choice.new_aggregated_payloads->length != 1 + || !client.fork_choice.new_aggregated_payloads->entries) { + fprintf(stderr, "fork choice new aggregated payload pool missing gossip proof\n"); + goto cleanup_attestation; + } + if (!client.fork_choice.attestation_data_by_root + || client.fork_choice.attestation_data_by_root->length != 1 + || !client.fork_choice.attestation_data_by_root->entries) { + fprintf(stderr, "fork choice attestation data map missing gossip attestation data\n"); + goto cleanup_attestation; + } + if (memcmp( + client.fork_choice.new_aggregated_payloads->entries[0].data_root.bytes, + data_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { + fprintf(stderr, "fork choice aggregated payload root mismatch\n"); + goto cleanup_attestation; + } + if (memcmp( + client.fork_choice.attestation_data_by_root->entries[0].data_root.bytes, + data_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { + fprintf(stderr, "fork choice attestation data root mismatch\n"); + goto cleanup_attestation; + } + if (client.fork_choice.attestation_data_by_root->entries[0].data.target.slot + != attestation.data.target.slot) { + fprintf(stderr, "fork choice attestation data target slot mismatch\n"); + goto cleanup_attestation; + } rc = 0; @@ -256,7 +294,8 @@ static int test_gossip_aggregated_attestation_rejects_invalid_proof(void) fprintf(stderr, "invalid aggregated attestation gossip should be ignored\n"); goto cleanup_attestation; } - if (client.agg_proof_cache.length != 0) { + if (client.store.new_aggregated_payloads.length != 0 + || client.store.known_aggregated_payloads.length != 0) { fprintf(stderr, "invalid aggregated attestation should not be cached\n"); goto cleanup_attestation; } @@ -308,7 +347,8 @@ static int test_gossip_aggregated_attestation_rejects_unknown_target(void) fprintf(stderr, "aggregated attestation with unknown target should be ignored\n"); goto cleanup_attestation; } - if (client.agg_proof_cache.length != 0) { + if (client.store.new_aggregated_payloads.length != 0 + || client.store.known_aggregated_payloads.length != 0) { fprintf(stderr, "unknown-target aggregated attestation should not be cached\n"); goto cleanup_attestation; } diff --git a/tests/unit/test_client_pending.c b/tests/unit/test_client_pending.c index 3312247..3ffcede 100644 --- a/tests/unit/test_client_pending.c +++ b/tests/unit/test_client_pending.c @@ -6,7 +6,313 @@ #include "client_test_helpers.h" #include "../../src/core/client_services_internal.h" +#include "lantern/consensus/duties.h" #include "lantern/consensus/hash.h" +#include "lantern/consensus/signature.h" +#include "lantern/consensus/state.h" +#include "lantern/genesis/genesis.h" +#include "lantern/storage/storage.h" + +enum { + TEST_TEMP_PATH_CAPACITY = 1024 +}; + +struct block_signature_fixture { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub; + struct PQSignatureSchemeSecretKey *secret; + struct lantern_validator_record *registry_records; + char data_dir_template[TEST_TEMP_PATH_CAPACITY]; +}; + +static int enable_signature_verification_registry( + struct lantern_client *client, + struct lantern_validator_record **out_records) +{ + if (!client || !out_records) { + return -1; + } + size_t validator_count = lantern_state_validator_count(&client->state); + if (validator_count == 0) { + return -1; + } + struct lantern_validator_record *records = calloc(validator_count, sizeof(*records)); + if (!records) { + return -1; + } + for (size_t i = 0; i < validator_count; ++i) { + records[i].index = (uint64_t)i; + const uint8_t *pubkey = lantern_state_validator_pubkey(&client->state, i); + if (!pubkey) { + free(records); + return -1; + } + memcpy(records[i].pubkey_bytes, pubkey, LANTERN_VALIDATOR_PUBKEY_SIZE); + records[i].has_pubkey_bytes = true; + } + client->genesis.validator_registry.records = records; + client->genesis.validator_registry.count = validator_count; + *out_records = records; + return 0; +} + +static void disable_signature_verification_registry( + struct lantern_client *client, + struct lantern_validator_record **records) +{ + if (!client || !records) { + return; + } + free(*records); + *records = NULL; + client->genesis.validator_registry.records = NULL; + client->genesis.validator_registry.count = 0; +} + +static int setup_block_signature_fixture( + struct block_signature_fixture *fixture, + const char *node_id) +{ + if (!fixture) { + return -1; + } + memset(fixture, 0, sizeof(*fixture)); + if (client_test_setup_vote_validation_client( + &fixture->client, + node_id, + &fixture->pub, + &fixture->secret, + NULL, + NULL) + != 0) { + return -1; + } + if (enable_signature_verification_registry(&fixture->client, &fixture->registry_records) != 0) { + client_test_teardown_vote_validation_client(&fixture->client, fixture->pub, fixture->secret); + fixture->pub = NULL; + fixture->secret = NULL; + return -1; + } + snprintf( + fixture->data_dir_template, + sizeof(fixture->data_dir_template), + "/tmp/lantern_block_sig_XXXXXX"); + fixture->client.data_dir = mkdtemp(fixture->data_dir_template); + if (!fixture->client.data_dir) { + disable_signature_verification_registry(&fixture->client, &fixture->registry_records); + client_test_teardown_vote_validation_client(&fixture->client, fixture->pub, fixture->secret); + fixture->pub = NULL; + fixture->secret = NULL; + return -1; + } + return 0; +} + +static void teardown_block_signature_fixture(struct block_signature_fixture *fixture) +{ + if (!fixture) { + return; + } + disable_signature_verification_registry(&fixture->client, &fixture->registry_records); + if (fixture->client.data_dir && fixture->client.data_dir[0] != '\0') { + char cleanup_cmd[TEST_TEMP_PATH_CAPACITY + 16]; + int written = snprintf(cleanup_cmd, sizeof(cleanup_cmd), "rm -rf %s", fixture->client.data_dir); + if (written > 0 && (size_t)written < sizeof(cleanup_cmd)) { + (void)system(cleanup_cmd); + } + } + fixture->client.data_dir = NULL; + client_test_teardown_vote_validation_client(&fixture->client, fixture->pub, fixture->secret); + fixture->pub = NULL; + fixture->secret = NULL; +} + +static int build_signed_block_for_import( + struct block_signature_fixture *fixture, + bool include_attestation_signature, + bool include_proposer_signature, + LanternSignedBlock *out_block, + LanternRoot *out_root) +{ + if (!fixture || !fixture->secret || !out_block || !out_root) { + return -1; + } + + lantern_signed_block_with_attestation_init(out_block); + + uint64_t block_slot = fixture->client.state.slot + 1u; + out_block->message.block.slot = block_slot; + if (lantern_proposer_for_slot( + block_slot, + fixture->client.state.config.num_validators, + &out_block->message.block.proposer_index) + != 0) { + return -1; + } + if (lantern_state_select_block_parent( + &fixture->client.state, + &fixture->client.store, + &out_block->message.block.parent_root) + != 0) { + return -1; + } + if (lantern_storage_store_state_for_root( + fixture->client.data_dir, + &out_block->message.block.parent_root, + &fixture->client.state) + != 0) { + return -1; + } + + LanternCheckpoint head = {0}; + LanternCheckpoint target = {0}; + LanternCheckpoint source = {0}; + if (lantern_state_compute_vote_checkpoints( + &fixture->client.state, + &fixture->client.store, + &head, + &target, + &source) + != 0) { + return -1; + } + + LanternSignedVote proposer_vote; + memset(&proposer_vote, 0, sizeof(proposer_vote)); + proposer_vote.data.validator_id = out_block->message.block.proposer_index; + proposer_vote.data.slot = 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, fixture->secret) != 0) { + return -1; + } + + out_block->message.proposer_attestation = proposer_vote.data; + if (include_proposer_signature) { + out_block->signatures.proposer_signature = proposer_vote.signature; + } else { + lantern_signature_zero(&out_block->signatures.proposer_signature); + } + + if (lantern_aggregated_attestations_resize(&out_block->message.block.body.attestations, 1u) != 0) { + return -1; + } + LanternAggregatedAttestation *attestation = &out_block->message.block.body.attestations.data[0]; + attestation->data = proposer_vote.data.data; + if (lantern_bitlist_resize(&attestation->aggregation_bits, 1u) != 0 + || lantern_bitlist_set(&attestation->aggregation_bits, 0u, true) != 0) { + return -1; + } + + if (include_attestation_signature) { + if (lantern_attestation_signatures_resize(&out_block->signatures.attestation_signatures, 1u) != 0) { + return -1; + } + LanternAggregatedSignatureProof *proof = &out_block->signatures.attestation_signatures.data[0]; + if (lantern_bitlist_resize(&proof->participants, 1u) != 0 + || lantern_bitlist_set(&proof->participants, 0u, true) != 0) { + return -1; + } + const uint8_t *pubkey = lantern_state_validator_pubkey(&fixture->client.state, 0u); + if (!pubkey) { + return -1; + } + LanternRoot attestation_root; + if (lantern_hash_tree_root_attestation_data(&attestation->data, &attestation_root) != 0) { + return -1; + } + const uint8_t *pubkeys[1] = {pubkey}; + LanternSignature signatures[1] = {proposer_vote.signature}; + if (!lantern_signature_aggregate( + pubkeys, + signatures, + 1u, + &attestation_root, + attestation->data.slot, + &proof->proof_data)) { + return -1; + } + } + + if (lantern_state_preview_post_state_root( + &fixture->client.state, + &fixture->client.store, + out_block, + &out_block->message.block.state_root) + != 0) { + return -1; + } + + return lantern_hash_tree_root_block(&out_block->message.block, out_root); +} + +static int resign_first_block_attestation( + struct block_signature_fixture *fixture, + LanternSignedBlock *block, + LanternRoot *out_root) +{ + if (!fixture || !fixture->secret || !block || !out_root) { + return -1; + } + if (block->message.block.body.attestations.length == 0 + || !block->message.block.body.attestations.data + || block->signatures.attestation_signatures.length == 0 + || !block->signatures.attestation_signatures.data) { + return -1; + } + + LanternAggregatedAttestation *attestation = &block->message.block.body.attestations.data[0]; + LanternAggregatedSignatureProof *proof = &block->signatures.attestation_signatures.data[0]; + + LanternSignedVote signed_vote; + memset(&signed_vote, 0, sizeof(signed_vote)); + signed_vote.data.validator_id = 0u; + signed_vote.data.slot = attestation->data.slot; + signed_vote.data.data = attestation->data; + if (client_test_sign_vote_with_secret(&signed_vote, fixture->secret) != 0) { + return -1; + } + + if (lantern_bitlist_resize(&attestation->aggregation_bits, 1u) != 0 + || lantern_bitlist_set(&attestation->aggregation_bits, 0u, true) != 0) { + return -1; + } + if (lantern_bitlist_resize(&proof->participants, 1u) != 0 + || lantern_bitlist_set(&proof->participants, 0u, true) != 0) { + return -1; + } + + const uint8_t *pubkey = lantern_state_validator_pubkey(&fixture->client.state, 0u); + if (!pubkey) { + return -1; + } + LanternRoot attestation_root; + if (lantern_hash_tree_root_attestation_data(&attestation->data, &attestation_root) != 0) { + return -1; + } + const uint8_t *pubkeys[1] = {pubkey}; + LanternSignature signatures[1] = {signed_vote.signature}; + if (!lantern_signature_aggregate( + pubkeys, + signatures, + 1u, + &attestation_root, + attestation->data.slot, + &proof->proof_data)) { + return -1; + } + + if (lantern_state_preview_post_state_root( + &fixture->client.state, + &fixture->client.store, + block, + &block->message.block.state_root) + != 0) { + return -1; + } + return lantern_hash_tree_root_block(&block->message.block, out_root); +} static int test_pending_block_queue(void) { struct lantern_client client; @@ -400,6 +706,7 @@ static int test_import_block_parent_mismatch(void) { lantern_state_init(&client.state); client.has_state = true; client.state.slot = 0; + lantern_store_init(&client.store); memset(&client.state.latest_block_header, 0, sizeof(client.state.latest_block_header)); client_test_fill_root(&client.state.latest_block_header.state_root, 0x10); @@ -415,17 +722,22 @@ static int test_import_block_parent_mismatch(void) { } lantern_fork_choice_init(&client.fork_choice); + lantern_store_attach_fork_choice(&client.store, &client.fork_choice); LanternConfig fork_cfg = { .num_validators = 8, .genesis_time = 0, }; + if (lantern_store_prepare_fork_choice_votes(&client.store, fork_cfg.num_validators) != 0) { + fprintf(stderr, "failed to prepare fork choice votes\n"); + rc = 1; + goto cleanup; + } if (lantern_fork_choice_configure(&client.fork_choice, &fork_cfg) != 0) { fprintf(stderr, "failed to configure fork choice\n"); rc = 1; goto cleanup; } client.has_fork_choice = true; - lantern_state_attach_fork_choice(&client.state, &client.fork_choice); LanternCheckpoint anchor_checkpoint = { .root = head_root, @@ -571,6 +883,7 @@ static int test_import_block_parent_mismatch(void) { lantern_state_reset(&client.state); client.has_state = false; } + lantern_store_reset(&client.store); return rc; } @@ -675,6 +988,169 @@ static int test_reqresp_collect_blocks_pending_fallback(void) { return rc; } +static int test_import_block_accepts_complete_signatures(void) +{ + struct block_signature_fixture fixture; + LanternSignedBlock block; + LanternRoot block_root; + uint64_t initial_slot = 0; + int rc = 1; + + memset(&block, 0, sizeof(block)); + if (setup_block_signature_fixture(&fixture, "test_import_complete_signatures") != 0) { + fprintf(stderr, "failed to set up block signature fixture\n"); + return 1; + } + + initial_slot = fixture.client.state.slot; + if (build_signed_block_for_import(&fixture, true, true, &block, &block_root) != 0) { + fprintf(stderr, "failed to build fully signed block fixture\n"); + goto cleanup; + } + + if (lantern_client_debug_import_block(&fixture.client, &block, &block_root, "12D3KooWsig") != 1) { + fprintf(stderr, "import rejected block with complete signatures\n"); + goto cleanup; + } + if (fixture.client.state.slot != block.message.block.slot || fixture.client.state.slot == initial_slot) { + fprintf(stderr, "state slot did not advance after importing fully signed block\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_signed_block_with_attestation_reset(&block); + teardown_block_signature_fixture(&fixture); + return rc; +} + +static int test_import_block_rejects_missing_attestation_signature_groups(void) +{ + struct block_signature_fixture fixture; + LanternSignedBlock block; + LanternRoot block_root; + uint64_t initial_slot = 0; + int rc = 1; + + memset(&block, 0, sizeof(block)); + if (setup_block_signature_fixture(&fixture, "test_import_missing_att_sigs") != 0) { + fprintf(stderr, "failed to set up missing attestation signature fixture\n"); + return 1; + } + + initial_slot = fixture.client.state.slot; + if (build_signed_block_for_import(&fixture, false, true, &block, &block_root) != 0) { + fprintf(stderr, "failed to build block fixture without attestation signatures\n"); + goto cleanup; + } + + if (lantern_client_debug_import_block(&fixture.client, &block, &block_root, "12D3KooWsig") != 0) { + fprintf(stderr, "import unexpectedly accepted block missing attestation signatures\n"); + goto cleanup; + } + if (fixture.client.state.slot != initial_slot) { + fprintf(stderr, "state slot advanced after rejecting missing attestation signatures\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_signed_block_with_attestation_reset(&block); + teardown_block_signature_fixture(&fixture); + return rc; +} + +static int test_import_block_rejects_missing_proposer_signature(void) +{ + struct block_signature_fixture fixture; + LanternSignedBlock block; + LanternRoot block_root; + uint64_t initial_slot = 0; + int rc = 1; + + memset(&block, 0, sizeof(block)); + if (setup_block_signature_fixture(&fixture, "test_import_missing_prop_sig") != 0) { + fprintf(stderr, "failed to set up missing proposer signature fixture\n"); + return 1; + } + + initial_slot = fixture.client.state.slot; + if (build_signed_block_for_import(&fixture, true, false, &block, &block_root) != 0) { + fprintf(stderr, "failed to build block fixture without proposer signature\n"); + goto cleanup; + } + + if (lantern_client_debug_import_block(&fixture.client, &block, &block_root, "12D3KooWsig") != 0) { + fprintf(stderr, "import unexpectedly accepted block missing proposer signature\n"); + goto cleanup; + } + if (fixture.client.state.slot != initial_slot) { + fprintf(stderr, "state slot advanced after rejecting missing proposer signature\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_signed_block_with_attestation_reset(&block); + teardown_block_signature_fixture(&fixture); + return rc; +} + +static int test_import_block_skips_unknown_attestation_head_root(void) +{ + struct block_signature_fixture fixture; + LanternSignedBlock block; + LanternRoot block_root; + LanternRoot unknown_root; + uint64_t initial_slot = 0; + int rc = 1; + + memset(&block, 0, sizeof(block)); + if (setup_block_signature_fixture(&fixture, "test_import_unknown_att_head") != 0) { + fprintf(stderr, "failed to set up unknown attestation head 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 unknown attestation head test\n"); + goto cleanup; + } + initial_slot = fixture.client.state.slot; + + client_test_fill_root(&unknown_root, 0xD4); + if (memcmp( + unknown_root.bytes, + block.message.block.body.attestations.data[0].data.head.root.bytes, + LANTERN_ROOT_SIZE) + == 0) { + unknown_root.bytes[0] ^= 0xFFu; + } + block.message.block.body.attestations.data[0].data.head.root = unknown_root; + if (resign_first_block_attestation(&fixture, &block, &block_root) != 0) { + fprintf(stderr, "failed to resign block fixture with unknown attestation head\n"); + goto cleanup; + } + + if (lantern_client_debug_import_block(&fixture.client, &block, &block_root, "12D3KooWsig") != 1) { + fprintf(stderr, "import rejected block with unknown attestation head root\n"); + goto cleanup; + } + if (fixture.client.state.slot != block.message.block.slot || fixture.client.state.slot == initial_slot) { + fprintf(stderr, "state slot did not advance after skipping unknown attestation head root\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; @@ -688,6 +1164,18 @@ int main(void) { if (test_reqresp_collect_blocks_pending_fallback() != 0) { return 1; } + if (test_import_block_accepts_complete_signatures() != 0) { + return 1; + } + if (test_import_block_rejects_missing_attestation_signature_groups() != 0) { + return 1; + } + if (test_import_block_rejects_missing_proposer_signature() != 0) { + return 1; + } + if (test_import_block_skips_unknown_attestation_head_root() != 0) { + return 1; + } puts("lantern_client_pending_test OK"); return 0; } diff --git a/tests/unit/test_client_vote.c b/tests/unit/test_client_vote.c index d2848d7..0ee0272 100644 --- a/tests/unit/test_client_vote.c +++ b/tests/unit/test_client_vote.c @@ -11,34 +11,52 @@ #include "lantern/support/time.h" /* Internal core APIs used for targeted cache and block-build regression tests. */ -int lantern_client_agg_proof_cache_add( +int lantern_client_set_gossip_signature( + struct lantern_client *client, + const LanternSignatureKey *key, + const LanternAttestationData *data, + const LanternSignature *signature, + uint64_t target_slot); +int lantern_client_add_new_aggregated_payload( struct lantern_client *client, const LanternRoot *data_root, + const LanternAttestationData *data, const LanternAggregatedSignatureProof *proof, uint64_t target_slot); -size_t lantern_client_agg_proof_cache_prune_finalized( +int lantern_client_add_known_aggregated_payload( + struct lantern_client *client, + const LanternRoot *data_root, + const LanternAttestationData *data, + const LanternAggregatedSignatureProof *proof, + uint64_t target_slot); +size_t lantern_client_promote_new_aggregated_payloads( + struct lantern_client *client); +size_t lantern_client_prune_finalized_attestation_material( struct lantern_client *client, uint64_t finalized_slot); +int lantern_client_chain_service_tick_to( + struct lantern_client *client, + uint64_t target_interval, + bool has_proposal, + uint64_t *out_skipped_to_interval, + uint64_t *out_ticked_intervals); +int lantern_client_advance_fork_choice_time_locked( + struct lantern_client *client, + uint64_t now_milliseconds, + bool has_proposal); lantern_client_error lantern_client_aggregate_attestations_for_block( struct lantern_client *client, const LanternAttestations *att_list, const LanternSignatureList *att_signatures, LanternAggregatedAttestations *out_attestations, LanternAttestationSignatures *out_signatures); +int validator_publish_attestations(struct lantern_client *client, uint64_t slot); static void test_reset_agg_cache(struct lantern_client *client) { if (!client) { return; } - if (client->agg_proof_cache.entries) { - for (size_t i = 0; i < client->agg_proof_cache.length; ++i) { - lantern_aggregated_signature_proof_reset(&client->agg_proof_cache.entries[i].proof); - } - } - free(client->agg_proof_cache.entries); - client->agg_proof_cache.entries = NULL; - client->agg_proof_cache.length = 0; - client->agg_proof_cache.capacity = 0; + lantern_store_reset(&client->store); } static int test_make_dummy_proof( @@ -68,6 +86,19 @@ static int test_make_dummy_proof( return 0; } +static LanternAttestationData test_make_attestation_data(uint64_t slot, uint8_t marker) { + LanternAttestationData data; + memset(&data, 0, sizeof(data)); + data.slot = slot; + data.head.slot = slot; + data.target.slot = slot; + data.source.slot = slot == 0 ? 0 : slot - 1u; + memset(data.head.root.bytes, marker, LANTERN_ROOT_SIZE); + memset(data.target.root.bytes, (int)(marker + 1u), LANTERN_ROOT_SIZE); + memset(data.source.root.bytes, (int)(marker + 2u), LANTERN_ROOT_SIZE); + return data; +} + static bool proof_payload_equals( const LanternAggregatedSignatureProof *lhs, const LanternAggregatedSignatureProof *rhs) { @@ -141,6 +172,25 @@ static void publish_capture_reset(struct publish_capture *capture) { memset(capture, 0, sizeof(*capture)); } +static int advance_client_fork_choice_intervals( + struct lantern_client *client, + size_t count, + bool has_proposal) { + if (!client || !client->has_fork_choice || client->fork_choice.milliseconds_per_interval == 0) { + return -1; + } + for (size_t i = 0; i < count; ++i) { + uint64_t next_interval = client->fork_choice.time_intervals + 1u; + uint64_t now = + (client->fork_choice.config.genesis_time * 1000u) + + (next_interval * client->fork_choice.milliseconds_per_interval); + if (lantern_client_advance_fork_choice_time_locked(client, now, has_proposal) != 0) { + return -1; + } + } + return 0; +} + static int make_signed_vote_for_validator( struct lantern_client *client, struct PQSignatureSchemeSecretKey *secret, @@ -205,14 +255,14 @@ static int test_record_vote_accepts_known_roots(void) { goto cleanup; } - if (!lantern_state_validator_has_vote(&client.state, 0)) { + if (!lantern_store_validator_has_vote(&client.store, 0)) { fprintf(stderr, "known root vote was not stored\n"); goto cleanup; } LanternSignedVote stored; memset(&stored, 0, sizeof(stored)); - if (lantern_state_get_signed_validator_vote(&client.state, 0, &stored) != 0) { + if (lantern_store_get_signed_validator_vote(&client.store, 0, &stored) != 0) { fprintf(stderr, "failed to fetch stored vote\n"); goto cleanup; } @@ -225,6 +275,26 @@ static int test_record_vote_accepts_known_roots(void) { goto cleanup; } + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote.data.data, &data_root) != 0) { + fprintf(stderr, "failed to hash vote data for gossip signature cache\n"); + goto cleanup; + } + LanternSignatureKey key = { + .validator_index = vote.data.validator_id, + .data_root = data_root, + }; + LanternSignature cached_signature; + 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"); + goto cleanup; + } + if (memcmp(&cached_signature, &vote.signature, sizeof(vote.signature)) != 0) { + fprintf(stderr, "cached gossip signature mismatch\n"); + goto cleanup; + } + rc = 0; cleanup: @@ -232,6 +302,79 @@ static int test_record_vote_accepts_known_roots(void) { return rc; } +static int test_record_vote_rejects_missing_target_state(void) { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root; + LanternRoot child_root; + LanternBlock grandchild; + LanternRoot grandchild_root; + int rc = 1; + + memset(&grandchild, 0, sizeof(grandchild)); + memset(&grandchild_root, 0, sizeof(grandchild_root)); + + if (client_test_setup_vote_validation_client( + &client, + "vote_missing_target_state", + &pub, + &secret, + &anchor_root, + &child_root) + != 0) { + return 1; + } + + lantern_block_body_init(&grandchild.body); + uint64_t child_slot = 0; + if (client_test_slot_for_root(&client, &child_root, &child_slot) != 0) { + fprintf(stderr, "failed to resolve child slot for missing target state test\n"); + goto cleanup; + } + grandchild.slot = child_slot + 1u; + grandchild.proposer_index = 0u; + grandchild.parent_root = child_root; + client_test_fill_root(&grandchild.state_root, 0xC3u); + if (lantern_hash_tree_root_block(&grandchild, &grandchild_root) != 0) { + fprintf(stderr, "failed to hash grandchild block for missing target state test\n"); + goto cleanup; + } + if (lantern_fork_choice_add_block( + &client.fork_choice, + &grandchild, + NULL, + &client.state.latest_justified, + &client.state.latest_finalized, + &grandchild_root) + != 0) { + fprintf(stderr, "failed to add grandchild block for missing target state test\n"); + goto cleanup; + } + + LanternSignedVote vote; + if (make_signed_vote_for_validator(&client, secret, 0u, &anchor_root, &grandchild_root, &vote) != 0) { + fprintf(stderr, "failed to build signed vote for missing target state test\n"); + goto cleanup; + } + + if (lantern_client_debug_record_vote(&client, &vote, "vote_missing_state_peer") != 0) { + fprintf(stderr, "lantern_client_debug_record_vote failed for missing target state test\n"); + goto cleanup; + } + if (lantern_store_validator_has_vote(&client.store, 0u)) { + fprintf(stderr, "vote with missing target state should not be stored\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_block_body_reset(&grandchild.body); + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + static int test_record_vote_rejects_unknown_head(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; @@ -265,7 +408,7 @@ static int test_record_vote_rejects_unknown_head(void) { goto cleanup; } - if (lantern_state_validator_has_vote(&client.state, 0)) { + if (lantern_store_validator_has_vote(&client.store, 0)) { fprintf(stderr, "validator unexpectedly had a stored vote before test\n"); goto cleanup; } @@ -275,7 +418,7 @@ static int test_record_vote_rejects_unknown_head(void) { goto cleanup; } - if (lantern_state_validator_has_vote(&client.state, 0)) { + if (lantern_store_validator_has_vote(&client.store, 0)) { fprintf(stderr, "unknown head vote should not be stored\n"); goto cleanup; } @@ -332,7 +475,7 @@ static int test_record_vote_rejects_slot_mismatch(void) { lantern_client_debug_record_vote(&client, &vote, "slot_mismatch_peer"); - if (lantern_state_validator_has_vote(&client.state, 0)) { + if (lantern_store_validator_has_vote(&client.store, 0)) { fprintf(stderr, "slot mismatch vote should have been rejected\n"); goto cleanup; } @@ -395,7 +538,7 @@ static int test_record_vote_rejects_head_older_than_target(void) { lantern_client_debug_record_vote(&client, &vote, "head_older_peer"); - if (lantern_state_validator_has_vote(&client.state, 0)) { + if (lantern_store_validator_has_vote(&client.store, 0)) { fprintf(stderr, "head older than target vote should have been rejected\n"); goto cleanup; } @@ -462,7 +605,7 @@ static int test_record_vote_rejects_future_slot(void) { lantern_client_debug_record_vote(&client, &vote, "future_slot_peer"); - if (lantern_state_validator_has_vote(&client.state, 0)) { + if (lantern_store_validator_has_vote(&client.store, 0)) { fprintf(stderr, "future slot vote should have been dropped\n"); goto cleanup; } @@ -659,12 +802,24 @@ static int test_record_vote_defers_interval_pipeline(void) { struct PQSignatureSchemeSecretKey *secret = NULL; LanternRoot anchor_root; LanternRoot child_root; + struct lantern_validator_config_entry assigned; int rc = 1; + memset(&assigned, 0, sizeof(assigned)); + if (client_test_setup_vote_validation_client(&client, "vote_interval", &pub, &secret, &anchor_root, &child_root) != 0) { return 1; } + assigned.enr.is_aggregator = true; + client.assigned_validators = &assigned; + client.gossip.attestation_subnet_id = 0u; + snprintf( + client.gossip.aggregated_attestation_topic, + sizeof(client.gossip.aggregated_attestation_topic), + "test/vote_interval_aggregation"); + lantern_gossipsub_service_set_loopback_only(&client.gossip, 1); + LanternSignedVote vote; memset(&vote, 0, sizeof(vote)); uint64_t child_slot = 0; @@ -694,20 +849,45 @@ static int test_record_vote_defers_interval_pipeline(void) { goto cleanup; } + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote.data.data, &data_root) != 0) { + fprintf(stderr, "failed to hash vote data for interval pipeline test\n"); + goto cleanup; + } + LanternSignatureKey key = { + .validator_index = vote.data.validator_id, + .data_root = data_root, + }; + LanternSignature cached_signature; + 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 vote before aggregation\n"); + goto cleanup; + } + if (memcmp(&cached_signature, &vote.signature, sizeof(vote.signature)) != 0) { + fprintf(stderr, "cached gossip signature mismatch before aggregation\n"); + goto cleanup; + } + struct lantern_fork_choice_vote_entry *new_entry = client.fork_choice.new_votes; struct lantern_fork_choice_vote_entry *known_entry = client.fork_choice.known_votes; if (!new_entry || !known_entry) { fprintf(stderr, "fork choice vote tables unavailable for interval pipeline test\n"); goto cleanup; } - if (!new_entry->has_checkpoint) { - fprintf(stderr, "gossip vote missing from new_votes immediately after record\n"); + if (new_entry->has_checkpoint) { + fprintf(stderr, "gossip vote should not bypass aggregation via new_votes\n"); goto cleanup; } if (known_entry->has_checkpoint) { fprintf(stderr, "known_votes updated before interval pipeline advanced\n"); goto cleanup; } + if (client.store.new_aggregated_payloads.length != 0 + || client.store.known_aggregated_payloads.length != 0) { + fprintf(stderr, "aggregated payload pools should be empty before interval 2 aggregation\n"); + goto cleanup; + } if (had_safe_before) { if (memcmp(client.fork_choice.safe_target.bytes, safe_before.bytes, LANTERN_ROOT_SIZE) != 0) { fprintf(stderr, "safe target changed before interval 2\n"); @@ -715,12 +895,12 @@ static int test_record_vote_defers_interval_pipeline(void) { } } - if (client_test_advance_fork_choice_intervals(&client.fork_choice, 1, false) != 0) { + if (advance_client_fork_choice_intervals(&client, 1, false) != 0) { fprintf(stderr, "failed to advance fork choice to interval 1\n"); goto cleanup; } - if (!new_entry->has_checkpoint) { - fprintf(stderr, "new_votes lost checkpoint before interval 2\n"); + if (new_entry->has_checkpoint) { + fprintf(stderr, "new_votes updated before interval 2 aggregation\n"); goto cleanup; } if (known_entry->has_checkpoint) { @@ -734,18 +914,29 @@ static int test_record_vote_defers_interval_pipeline(void) { } } - if (client_test_advance_fork_choice_intervals(&client.fork_choice, 1, false) != 0) { + if (advance_client_fork_choice_intervals(&client, 1, false) != 0) { fprintf(stderr, "failed to advance fork choice to interval 2\n"); goto cleanup; } - if (!new_entry->has_checkpoint) { - fprintf(stderr, "new_votes lost checkpoint before interval 3\n"); + if (new_entry->has_checkpoint) { + fprintf(stderr, "new_votes updated before local aggregation proof exists\n"); goto cleanup; } if (known_entry->has_checkpoint) { fprintf(stderr, "known_votes filled before interval 3\n"); goto cleanup; } + client.validator_duty.slot_attested = true; + client.validator_duty.slot_aggregated = false; + if (lantern_client_debug_run_interval_aggregation(&client, vote.data.slot) != LANTERN_CLIENT_OK) { + fprintf(stderr, "interval 2 aggregation failed for staged gossip vote\n"); + goto cleanup; + } + if (client.store.new_aggregated_payloads.length != 1 + || client.store.known_aggregated_payloads.length != 0) { + fprintf(stderr, "interval 2 aggregation did not stage proof into new payload pool\n"); + goto cleanup; + } if (had_safe_before) { if (memcmp(client.fork_choice.safe_target.bytes, safe_before.bytes, LANTERN_ROOT_SIZE) != 0) { fprintf(stderr, "safe target changed during interval 2\n"); @@ -753,7 +944,7 @@ static int test_record_vote_defers_interval_pipeline(void) { } } - if (client_test_advance_fork_choice_intervals(&client.fork_choice, 1, false) != 0) { + if (advance_client_fork_choice_intervals(&client, 1, false) != 0) { fprintf(stderr, "failed to advance fork choice to interval 3\n"); goto cleanup; } @@ -765,8 +956,8 @@ static int test_record_vote_defers_interval_pipeline(void) { fprintf(stderr, "safe target did not reflect gossip vote after interval 3\n"); goto cleanup; } - if (!new_entry->has_checkpoint) { - fprintf(stderr, "new_votes checkpoint missing after interval 3\n"); + if (new_entry->has_checkpoint) { + fprintf(stderr, "new_votes should stay empty until proof acceptance\n"); goto cleanup; } if (known_entry->has_checkpoint) { @@ -774,7 +965,7 @@ static int test_record_vote_defers_interval_pipeline(void) { goto cleanup; } - if (client_test_advance_fork_choice_intervals(&client.fork_choice, 1, false) != 0) { + if (advance_client_fork_choice_intervals(&client, 1, false) != 0) { fprintf(stderr, "failed to advance fork choice to interval 4\n"); goto cleanup; } @@ -791,6 +982,71 @@ static int test_record_vote_defers_interval_pipeline(void) { fprintf(stderr, "new_votes retained checkpoint after migration\n"); goto cleanup; } + if (client.store.new_aggregated_payloads.length != 0 + || client.store.known_aggregated_payloads.length != 1) { + fprintf(stderr, "aggregated payload pools did not migrate after interval 4\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + +static int test_chain_service_tick_to_skips_stale_intervals(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, "chain_service_skip", &pub, &secret, &anchor_root, &child_root) != 0) { + return 1; + } + + uint64_t intervals_per_slot = client.fork_choice.intervals_per_slot; + uint64_t target_interval = intervals_per_slot * 2u; + uint64_t skipped_to_interval = UINT64_MAX; + uint64_t ticked_intervals = 0u; + + if (intervals_per_slot == 0u) { + fprintf(stderr, "intervals_per_slot unavailable for chain service skip test\n"); + goto cleanup; + } + if (lantern_client_chain_service_tick_to( + &client, + target_interval, + false, + &skipped_to_interval, + &ticked_intervals) + != 0) { + fprintf(stderr, "chain service catch-up failed\n"); + goto cleanup; + } + if (skipped_to_interval != target_interval - intervals_per_slot) { + fprintf(stderr, + "chain service did not skip stale intervals correctly: expected %" PRIu64 " got %" PRIu64 "\n", + target_interval - intervals_per_slot, + skipped_to_interval); + goto cleanup; + } + if (ticked_intervals != intervals_per_slot) { + fprintf(stderr, + "chain service tick count mismatch after skip: expected %" PRIu64 " got %" PRIu64 "\n", + intervals_per_slot, + ticked_intervals); + goto cleanup; + } + if (client.fork_choice.time_intervals != target_interval) { + fprintf(stderr, + "fork choice time mismatch after chain service catch-up: expected %" PRIu64 " got %" PRIu64 "\n", + target_interval, + client.fork_choice.time_intervals); + goto cleanup; + } rc = 0; @@ -799,66 +1055,467 @@ static int test_record_vote_defers_interval_pipeline(void) { return rc; } -static int test_agg_proof_cache_prunes_finalized_entries(void) { +static int test_safe_target_uses_attached_aggregated_payloads(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_with_validator_count( + &client, + "safe_target_aggregated", + 3u, + &pub, + &secret, + &anchor_root, + &child_root) + != 0) { + return 1; + } + + uint64_t child_slot = 0u; + if (client_test_slot_for_root(&client, &child_root, &child_slot) != 0) { + fprintf(stderr, "failed to resolve child slot for aggregated safe-target test\n"); + goto cleanup; + } + + LanternAttestationData data; + memset(&data, 0, sizeof(data)); + data.slot = child_slot; + data.head.slot = child_slot; + data.head.root = child_root; + data.target.slot = child_slot; + data.target.root = child_root; + data.source.slot = 0u; + data.source.root = anchor_root; + + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&data, &data_root) != 0) { + fprintf(stderr, "failed to hash attestation data for aggregated safe-target test\n"); + goto cleanup; + } + + LanternAggregatedSignatureProof known_proof; + LanternAggregatedSignatureProof new_proof; + if (test_make_dummy_proof(&known_proof, 0u, 0x51) != 0) { + fprintf(stderr, "failed to build known aggregated proof for safe-target test\n"); + goto cleanup; + } + if (test_make_dummy_proof(&new_proof, 1u, 0x61) != 0) { + fprintf(stderr, "failed to build new aggregated proof for safe-target test\n"); + lantern_aggregated_signature_proof_reset(&known_proof); + goto cleanup; + } + + if (lantern_client_add_known_aggregated_payload(&client, &data_root, &data, &known_proof, data.target.slot) != 0) { + fprintf(stderr, "failed to add known aggregated payload for safe-target test\n"); + lantern_aggregated_signature_proof_reset(&new_proof); + lantern_aggregated_signature_proof_reset(&known_proof); + goto cleanup; + } + if (lantern_client_add_new_aggregated_payload(&client, &data_root, &data, &new_proof, data.target.slot) != 0) { + fprintf(stderr, "failed to add new aggregated payload for safe-target test\n"); + lantern_aggregated_signature_proof_reset(&new_proof); + lantern_aggregated_signature_proof_reset(&known_proof); + goto cleanup; + } + lantern_aggregated_signature_proof_reset(&new_proof); + lantern_aggregated_signature_proof_reset(&known_proof); + + if (lantern_fork_choice_update_safe_target(&client.fork_choice) != 0) { + fprintf(stderr, "failed to update safe target from attached aggregated payloads\n"); + goto cleanup; + } + if (!client.fork_choice.has_safe_target) { + fprintf(stderr, "safe target missing after aggregated payload update\n"); + goto cleanup; + } + if (memcmp(client.fork_choice.safe_target.bytes, child_root.bytes, LANTERN_ROOT_SIZE) != 0) { + fprintf(stderr, "safe target did not count attached aggregated payload support\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + test_reset_agg_cache(&client); + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + +static int test_new_aggregated_payloads_promote_to_known(void) { struct lantern_client client; memset(&client, 0, sizeof(client)); + lantern_store_init(&client.store); + + LanternRoot data_root; + client_test_fill_root_with_index(&data_root, 0x303u); + + LanternAggregatedSignatureProof proof; + if (test_make_dummy_proof(&proof, 0, 0x33) != 0) { + return 1; + } + + LanternAttestationData data = test_make_attestation_data(6u, 0x44u); + int rc = 1; + if (lantern_client_add_new_aggregated_payload( + &client, + &data_root, + &data, + &proof, + data.target.slot) + != 0) { + fprintf(stderr, "failed to add pending aggregated payload\n"); + goto cleanup; + } + if (client.store.new_aggregated_payloads.length != 1 + || client.store.known_aggregated_payloads.length != 0) { + fprintf(stderr, "unexpected payload pool lengths before promotion\n"); + goto cleanup; + } + + size_t moved = lantern_client_promote_new_aggregated_payloads(&client); + if (moved != 1) { + fprintf(stderr, "expected one payload to migrate to known pool, got=%zu\n", moved); + goto cleanup; + } + if (client.store.new_aggregated_payloads.length != 0 + || client.store.known_aggregated_payloads.length != 1) { + fprintf(stderr, "unexpected payload pool lengths after promotion\n"); + goto cleanup; + } + if (memcmp( + client.store.known_aggregated_payloads.entries[0].data_root.bytes, + data_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { + fprintf(stderr, "known payload root mismatch after promotion\n"); + goto cleanup; + } + if (client.store.known_aggregated_payloads.entries[0].target_slot != data.target.slot) { + fprintf(stderr, "known payload target slot mismatch after promotion\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_aggregated_signature_proof_reset(&proof); + test_reset_agg_cache(&client); + return rc; +} + +static int test_attestation_material_prunes_finalized_entries(void) { + struct lantern_client client; + memset(&client, 0, sizeof(client)); + lantern_store_init(&client.store); + + LanternRoot stale_new_root; + LanternRoot stale_known_root; + LanternRoot fresh_new_root; + LanternRoot fresh_known_root; + client_test_fill_root_with_index(&stale_new_root, 0x101u); + client_test_fill_root_with_index(&stale_known_root, 0x202u); + client_test_fill_root_with_index(&fresh_new_root, 0x303u); + client_test_fill_root_with_index(&fresh_known_root, 0x404u); + + LanternAggregatedSignatureProof stale_new_proof; + LanternAggregatedSignatureProof stale_known_proof; + LanternAggregatedSignatureProof fresh_new_proof; + LanternAggregatedSignatureProof fresh_known_proof; + if (test_make_dummy_proof(&stale_new_proof, 0, 0x11) != 0) { + return 1; + } + if (test_make_dummy_proof(&stale_known_proof, 1, 0x22) != 0 + || test_make_dummy_proof(&fresh_new_proof, 2, 0x77) != 0 + || test_make_dummy_proof(&fresh_known_proof, 3, 0x88) != 0) { + lantern_aggregated_signature_proof_reset(&stale_new_proof); + lantern_aggregated_signature_proof_reset(&stale_known_proof); + lantern_aggregated_signature_proof_reset(&fresh_new_proof); + return 1; + } + + LanternAttestationData stale_new_data = test_make_attestation_data(3u, 0x10u); + LanternAttestationData stale_known_data = test_make_attestation_data(4u, 0x20u); + LanternAttestationData fresh_new_data = test_make_attestation_data(8u, 0x30u); + LanternAttestationData fresh_known_data = test_make_attestation_data(9u, 0x40u); + LanternSignature stale_signature; + LanternSignature fresh_signature; + memset(&stale_signature, 0x55, sizeof(stale_signature)); + memset(&fresh_signature, 0x66, sizeof(fresh_signature)); + + int rc = 1; + if (lantern_client_add_new_aggregated_payload( + &client, + &stale_new_root, + &stale_new_data, + &stale_new_proof, + stale_new_data.target.slot) + != 0) { + fprintf(stderr, "failed to add stale pending payload\n"); + goto cleanup; + } + if (lantern_client_add_known_aggregated_payload( + &client, + &stale_known_root, + &stale_known_data, + &stale_known_proof, + stale_known_data.target.slot) + != 0) { + fprintf(stderr, "failed to add stale known payload\n"); + goto cleanup; + } + if (lantern_client_add_new_aggregated_payload( + &client, + &fresh_new_root, + &fresh_new_data, + &fresh_new_proof, + fresh_new_data.target.slot) + != 0) { + fprintf(stderr, "failed to add fresh pending payload\n"); + goto cleanup; + } + if (lantern_client_add_known_aggregated_payload( + &client, + &fresh_known_root, + &fresh_known_data, + &fresh_known_proof, + fresh_known_data.target.slot) + != 0) { + fprintf(stderr, "failed to add fresh known payload\n"); + goto cleanup; + } + + LanternSignatureKey stale_key = { + .validator_index = 0u, + .data_root = stale_new_root, + }; + LanternSignatureKey fresh_key = { + .validator_index = 1u, + .data_root = fresh_known_root, + }; + if (lantern_client_set_gossip_signature( + &client, + &stale_key, + &stale_new_data, + &stale_signature, + stale_new_data.target.slot) + != 0 + || lantern_client_set_gossip_signature( + &client, + &fresh_key, + &fresh_known_data, + &fresh_signature, + fresh_known_data.target.slot) + != 0) { + fprintf(stderr, "failed to seed gossip signature cache\n"); + goto cleanup; + } + + if (client.store.new_aggregated_payloads.length != 2 + || client.store.known_aggregated_payloads.length != 2 + || client.store.gossip_signatures.length != 2) { + fprintf(stderr, "unexpected attestation material lengths before prune\n"); + goto cleanup; + } + + size_t removed = lantern_client_prune_finalized_attestation_material(&client, 5); + if (removed != 2) { + fprintf(stderr, "expected two stale payloads to be pruned, got=%zu\n", removed); + goto cleanup; + } + if (client.store.new_aggregated_payloads.length != 1 + || client.store.known_aggregated_payloads.length != 1 + || client.store.gossip_signatures.length != 1) { + fprintf(stderr, "unexpected attestation material lengths after prune\n"); + goto cleanup; + } + if (memcmp( + client.store.new_aggregated_payloads.entries[0].data_root.bytes, + fresh_new_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { + fprintf(stderr, "fresh pending payload root mismatch after prune\n"); + goto cleanup; + } + if (memcmp( + client.store.known_aggregated_payloads.entries[0].data_root.bytes, + fresh_known_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { + fprintf(stderr, "fresh known payload root mismatch after prune\n"); + goto cleanup; + } + if (client.store.new_aggregated_payloads.entries[0].target_slot != 8 + || client.store.known_aggregated_payloads.entries[0].target_slot != 9) { + fprintf(stderr, "fresh payload target slot mismatch after prune\n"); + goto cleanup; + } + if (client.store.gossip_signatures.entries[0].key.validator_index != fresh_key.validator_index + || memcmp( + client.store.gossip_signatures.entries[0].key.data_root.bytes, + fresh_key.data_root.bytes, + LANTERN_ROOT_SIZE) + != 0) { + fprintf(stderr, "fresh gossip signature key mismatch after prune\n"); + goto cleanup; + } + LanternAttestationData pruned_data; + if (lantern_store_get_attestation_data(&client.store, &stale_new_root, &pruned_data) == 0) { + fprintf(stderr, "stale attestation data should have been pruned\n"); + goto cleanup; + } + if (lantern_store_get_attestation_data(&client.store, &fresh_known_root, &pruned_data) != 0) { + fprintf(stderr, "fresh attestation data missing after prune\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_aggregated_signature_proof_reset(&stale_new_proof); + lantern_aggregated_signature_proof_reset(&stale_known_proof); + lantern_aggregated_signature_proof_reset(&fresh_new_proof); + lantern_aggregated_signature_proof_reset(&fresh_known_proof); + test_reset_agg_cache(&client); + return rc; +} + +static int test_attestation_material_prune_tracks_stale_data_roots(void) { + struct lantern_client client; + memset(&client, 0, sizeof(client)); + lantern_store_init(&client.store); LanternRoot stale_root; - LanternRoot fresh_root; - client_test_fill_root_with_index(&stale_root, 0x101u); - client_test_fill_root_with_index(&fresh_root, 0x202u); + LanternRoot orphan_root; + client_test_fill_root_with_index(&stale_root, 0x505u); + client_test_fill_root_with_index(&orphan_root, 0x606u); LanternAggregatedSignatureProof stale_proof; - LanternAggregatedSignatureProof fresh_proof; - if (test_make_dummy_proof(&stale_proof, 0, 0x11) != 0) { + LanternAggregatedSignatureProof orphan_proof; + if (test_make_dummy_proof(&stale_proof, 0u, 0x31) != 0) { return 1; } - if (test_make_dummy_proof(&fresh_proof, 1, 0x77) != 0) { + if (test_make_dummy_proof(&orphan_proof, 1u, 0x41) != 0) { lantern_aggregated_signature_proof_reset(&stale_proof); return 1; } + LanternAttestationData stale_data = test_make_attestation_data(3u, 0x51u); + LanternSignature stale_signature; + LanternSignature orphan_signature; + memset(&stale_signature, 0x71, sizeof(stale_signature)); + memset(&orphan_signature, 0x81, sizeof(orphan_signature)); + + LanternSignatureKey stale_key = { + .validator_index = 0u, + .data_root = stale_root, + }; + LanternSignatureKey orphan_key = { + .validator_index = 1u, + .data_root = orphan_root, + }; + int rc = 1; - if (lantern_client_agg_proof_cache_add(&client, &stale_root, &stale_proof, 3) != 0) { - fprintf(stderr, "failed to add stale cache entry\n"); + if (lantern_client_add_new_aggregated_payload( + &client, + &stale_root, + &stale_data, + &stale_proof, + stale_data.target.slot) + != 0) { + fprintf(stderr, "failed to add stale payload for root-tracking prune test\n"); goto cleanup; } - if (lantern_client_agg_proof_cache_add(&client, &fresh_root, &fresh_proof, 8) != 0) { - fprintf(stderr, "failed to add fresh cache entry\n"); + if (lantern_client_add_known_aggregated_payload( + &client, + &orphan_root, + NULL, + &orphan_proof, + 2u) + != 0) { + fprintf(stderr, "failed to add orphan payload for root-tracking prune test\n"); + goto cleanup; + } + if (lantern_client_set_gossip_signature( + &client, + &stale_key, + &stale_data, + &stale_signature, + stale_data.target.slot) + != 0 + || lantern_client_set_gossip_signature( + &client, + &orphan_key, + NULL, + &orphan_signature, + 2u) + != 0) { + fprintf(stderr, "failed to seed gossip signatures for root-tracking prune test\n"); goto cleanup; } - if (client.agg_proof_cache.length != 2) { - fprintf(stderr, "unexpected cache length before prune: %zu\n", client.agg_proof_cache.length); + + if (client.store.attestation_data_by_root.length != 1 + || client.store.new_aggregated_payloads.length != 1 + || client.store.known_aggregated_payloads.length != 1 + || client.store.gossip_signatures.length != 2) { + fprintf(stderr, "unexpected cache lengths before root-tracking prune test\n"); goto cleanup; } - size_t removed = lantern_client_agg_proof_cache_prune_finalized(&client, 5); - if (removed != 1) { - fprintf(stderr, "expected one stale cache entry to be pruned, got=%zu\n", removed); + size_t removed = lantern_client_prune_finalized_attestation_material(&client, 5u); + if (removed != 1u) { + fprintf(stderr, "expected one stale data root to be pruned, got=%zu\n", removed); goto cleanup; } - if (client.agg_proof_cache.length != 1) { - fprintf(stderr, "unexpected cache length after prune: %zu\n", client.agg_proof_cache.length); + if (client.store.attestation_data_by_root.length != 0 + || client.store.new_aggregated_payloads.length != 0 + || client.store.known_aggregated_payloads.length != 1 + || client.store.gossip_signatures.length != 1) { + fprintf(stderr, "unexpected cache lengths after root-tracking prune test\n"); goto cleanup; } if (memcmp( - client.agg_proof_cache.entries[0].data_root.bytes, - fresh_root.bytes, + client.store.known_aggregated_payloads.entries[0].data_root.bytes, + orphan_root.bytes, LANTERN_ROOT_SIZE) != 0) { - fprintf(stderr, "fresh cache entry root mismatch after prune\n"); + fprintf(stderr, "orphan payload root mismatch after root-tracking prune test\n"); + goto cleanup; + } + + LanternSignature cached_signature; + memset(&cached_signature, 0, sizeof(cached_signature)); + if (lantern_store_get_gossip_signature(&client.store, &orphan_key, &cached_signature) != 0) { + fprintf(stderr, "orphan gossip signature should remain after root-tracking prune test\n"); + goto cleanup; + } + if (memcmp(&cached_signature, &orphan_signature, sizeof(cached_signature)) != 0) { + fprintf(stderr, "orphan gossip signature mismatch after root-tracking prune test\n"); + goto cleanup; + } + if (lantern_store_get_gossip_signature(&client.store, &stale_key, &cached_signature) == 0) { + fprintf(stderr, "stale gossip signature should have been pruned by root-tracking test\n"); + goto cleanup; + } + + LanternAttestationData cached_data; + if (lantern_store_get_attestation_data(&client.store, &stale_root, &cached_data) == 0) { + fprintf(stderr, "stale attestation data should have been pruned in root-tracking test\n"); goto cleanup; } - if (client.agg_proof_cache.entries[0].target_slot != 8) { - fprintf(stderr, "fresh cache entry target slot mismatch after prune\n"); + if (lantern_store_get_attestation_data(&client.store, &orphan_root, &cached_data) == 0) { + fprintf(stderr, "orphan payload should not synthesize attestation data in root-tracking test\n"); goto cleanup; } rc = 0; cleanup: - lantern_aggregated_signature_proof_reset(&fresh_proof); lantern_aggregated_signature_proof_reset(&stale_proof); + lantern_aggregated_signature_proof_reset(&orphan_proof); test_reset_agg_cache(&client); return rc; } @@ -949,9 +1606,10 @@ static int test_validator_build_reuses_cached_group_proof(void) { } lantern_byte_list_reset(&aggregated_proof_bytes); - if (lantern_client_agg_proof_cache_add( + if (lantern_client_add_known_aggregated_payload( &client, &data_root, + &valid_vote.data.data, &cached_proof, valid_vote.data.target.slot) != 0) { @@ -1007,6 +1665,74 @@ static int test_validator_build_reuses_cached_group_proof(void) { return rc; } +static int test_validator_build_skips_raw_signatures_without_cached_proof(void) { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root; + LanternRoot child_root; + LanternSignedVote vote; + LanternAttestations att_list; + LanternSignatureList att_signatures; + LanternAggregatedAttestations out_attestations; + LanternAttestationSignatures out_signatures; + int rc = 1; + + memset(&vote, 0, sizeof(vote)); + lantern_attestations_init(&att_list); + lantern_signature_list_init(&att_signatures); + lantern_aggregated_attestations_init(&out_attestations); + lantern_attestation_signatures_init(&out_signatures); + + if (client_test_setup_vote_validation_client( + &client, + "vote_raw_signature_skip", + &pub, + &secret, + &anchor_root, + &child_root) + != 0) { + return 1; + } + + if (make_signed_vote_for_validator(&client, secret, 0u, &anchor_root, &child_root, &vote) != 0) { + fprintf(stderr, "failed to build signed vote for raw-signature skip test\n"); + goto cleanup; + } + + if (lantern_attestations_append(&att_list, &vote.data) != 0 + || lantern_signature_list_append(&att_signatures, &vote.signature) != 0) { + fprintf(stderr, "failed to prepare attestation input for raw-signature skip test\n"); + goto cleanup; + } + + lantern_client_error agg_rc = lantern_client_aggregate_attestations_for_block( + &client, + &att_list, + &att_signatures, + &out_attestations, + &out_signatures); + if (agg_rc != LANTERN_CLIENT_OK) { + fprintf(stderr, "cache-only aggregation failed rc=%d\n", (int)agg_rc); + goto cleanup; + } + if (out_attestations.length != 0 || out_signatures.length != 0) { + fprintf(stderr, "block aggregation should ignore uncached raw signatures\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_attestation_signatures_reset(&out_signatures); + lantern_aggregated_attestations_reset(&out_attestations); + lantern_signature_list_reset(&att_signatures); + lantern_attestations_reset(&att_list); + test_reset_agg_cache(&client); + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + static int test_publish_aggregated_attestations_filters_cross_subnet_votes(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; @@ -1092,6 +1818,94 @@ static int test_publish_aggregated_attestations_filters_cross_subnet_votes(void) return rc; } +static int test_publish_attestations_skips_proposer_pending_vote(void) { + struct lantern_client client; + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root; + LanternRoot child_root; + struct publish_capture capture; + struct lantern_local_validator validator; + bool validator_enabled = true; + LanternSignedVote proposer_vote; + int rc = 1; + + memset(&capture, 0, sizeof(capture)); + memset(&validator, 0, sizeof(validator)); + memset(&proposer_vote, 0, sizeof(proposer_vote)); + + if (client_test_setup_vote_validation_client( + &client, + "vote_skip_proposer_pending", + &pub, + &secret, + &anchor_root, + &child_root) + != 0) { + goto cleanup; + } + + if (make_signed_vote_for_validator(&client, secret, 0u, &anchor_root, &child_root, &proposer_vote) != 0) { + fprintf(stderr, "failed to build proposer pending vote for skip test\n"); + goto cleanup; + } + + validator.global_index = proposer_vote.data.validator_id; + validator.last_proposed_slot = proposer_vote.data.slot; + validator.last_attested_slot = UINT64_MAX; + validator.pending_attestation = proposer_vote; + validator.pending_attestation_slot = proposer_vote.data.slot; + validator.has_pending_attestation = true; + + client.local_validators = &validator; + client.local_validator_count = 1u; + client.validator_enabled = &validator_enabled; + client.has_runtime = true; + client.gossip_running = true; + client.gossip.attestation_subnet_id = 0u; + snprintf(client.gossip.vote_topic, sizeof(client.gossip.vote_topic), "test/skip_proposer_vote"); + snprintf( + client.gossip.vote_subnet_topic, + sizeof(client.gossip.vote_subnet_topic), + "test/skip_proposer_vote_subnet"); + lantern_gossipsub_service_set_publish_hook(&client.gossip, publish_capture_hook, &capture); + lantern_gossipsub_service_set_loopback_only(&client.gossip, 1); + + if (lantern_store_validator_has_vote(&client.store, proposer_vote.data.validator_id)) { + fprintf(stderr, "validator vote cache unexpectedly populated before proposer skip test\n"); + goto cleanup; + } + + if (validator_publish_attestations(&client, proposer_vote.data.slot) != LANTERN_CLIENT_OK) { + fprintf(stderr, "validator_publish_attestations failed for proposer skip test\n"); + goto cleanup; + } + if (capture.calls != 0u) { + fprintf(stderr, "proposer should not republish a pending attestation at interval 1\n"); + goto cleanup; + } + if (lantern_store_validator_has_vote(&client.store, proposer_vote.data.validator_id)) { + fprintf(stderr, "proposer pending vote should not be staged into the validator vote cache\n"); + goto cleanup; + } + if (validator.last_attested_slot != proposer_vote.data.slot) { + fprintf(stderr, "proposer skip path did not mark slot %" PRIu64 " as attested\n", proposer_vote.data.slot); + goto cleanup; + } + if (validator.has_pending_attestation || validator.pending_attestation_slot != UINT64_MAX) { + fprintf(stderr, "proposer pending attestation was not cleared after interval-1 skip\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + publish_capture_reset(&capture); + test_reset_agg_cache(&client); + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + static int test_interval_2_aggregation_trigger_respects_aggregator_role(void) { struct lantern_client client; struct PQSignatureSchemePublicKey *pub = NULL; @@ -1175,6 +1989,9 @@ int main(void) { if (test_record_vote_accepts_known_roots() != 0) { return 1; } + if (test_record_vote_rejects_missing_target_state() != 0) { + return 1; + } if (test_record_vote_rejects_unknown_head() != 0) { return 1; } @@ -1196,12 +2013,30 @@ int main(void) { if (test_record_vote_defers_interval_pipeline() != 0) { return 1; } - if (test_agg_proof_cache_prunes_finalized_entries() != 0) { + if (test_chain_service_tick_to_skips_stale_intervals() != 0) { + return 1; + } + if (test_safe_target_uses_attached_aggregated_payloads() != 0) { + return 1; + } + if (test_new_aggregated_payloads_promote_to_known() != 0) { + return 1; + } + if (test_attestation_material_prunes_finalized_entries() != 0) { + return 1; + } + if (test_attestation_material_prune_tracks_stale_data_roots() != 0) { + return 1; + } + if (test_validator_build_skips_raw_signatures_without_cached_proof() != 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) { return 1; } diff --git a/tests/unit/test_fork_choice.c b/tests/unit/test_fork_choice.c index 126c2d3..bfcea12 100644 --- a/tests/unit/test_fork_choice.c +++ b/tests/unit/test_fork_choice.c @@ -2,10 +2,12 @@ #include #include #include +#include #include #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/hash.h" +#include "lantern/consensus/store.h" #include "lantern/consensus/state.h" static void zero_root(LanternRoot *root) { @@ -99,9 +101,13 @@ static LanternSignedVote make_vote( static int test_fork_choice_proposer_attestation_sequence(void) { LanternForkChoice store; + LanternStore backing_store; lantern_fork_choice_init(&store); + lantern_store_init(&backing_store); + lantern_store_attach_fork_choice(&backing_store, &store); LanternConfig config = {.num_validators = 1, .genesis_time = 10}; + assert(lantern_store_prepare_fork_choice_votes(&backing_store, config.num_validators) == 0); assert(lantern_fork_choice_configure(&store, &config) == 0); LanternBlock genesis; @@ -129,26 +135,18 @@ static int test_fork_choice_proposer_attestation_sequence(void) { assert(store.new_votes); assert(store.known_votes); - assert(store.new_votes[0].has_checkpoint); - assert(roots_equal(&store.new_votes[0].checkpoint.root, &block_one_root)); - assert(store.new_votes[0].checkpoint.slot == block_one.slot); + assert(!store.new_votes[0].has_checkpoint); assert(!store.known_votes[0].has_checkpoint); - assert(lantern_fork_choice_update_safe_target(&store) == 0); - const LanternRoot *safe_target = lantern_fork_choice_safe_target(&store); - assert(safe_target && roots_equal(safe_target, &block_one_root)); - LanternRoot head; assert(lantern_fork_choice_current_head(&store, &head) == 0); assert(roots_equal(&head, &genesis_root)); assert(lantern_fork_choice_accept_new_votes(&store) == 0); assert(!store.new_votes[0].has_checkpoint); - assert(store.known_votes[0].has_checkpoint); - assert(store.known_votes[0].checkpoint.slot == block_one.slot); - assert(roots_equal(&store.known_votes[0].checkpoint.root, &block_one_root)); + assert(!store.known_votes[0].has_checkpoint); assert(lantern_fork_choice_current_head(&store, &head) == 0); - assert(roots_equal(&head, &block_one_root)); + assert(roots_equal(&head, &genesis_root)); LanternBlock block_two; init_block(&block_two, 2, 0, &block_one_root, 0xCC); @@ -166,25 +164,19 @@ static int test_fork_choice_proposer_attestation_sequence(void) { &block_two_root) == 0); - assert(store.new_votes[0].has_checkpoint); - assert(store.new_votes[0].checkpoint.slot == block_two.slot); - assert(roots_equal(&store.new_votes[0].checkpoint.root, &block_two_root)); - assert(store.known_votes[0].checkpoint.slot == block_one.slot); - - assert(lantern_fork_choice_update_safe_target(&store) == 0); - safe_target = lantern_fork_choice_safe_target(&store); - assert(safe_target && roots_equal(safe_target, &block_two_root)); + assert(!store.new_votes[0].has_checkpoint); + assert(!store.known_votes[0].has_checkpoint); assert(lantern_fork_choice_current_head(&store, &head) == 0); - assert(roots_equal(&head, &block_one_root)); + assert(roots_equal(&head, &genesis_root)); assert(lantern_fork_choice_accept_new_votes(&store) == 0); assert(!store.new_votes[0].has_checkpoint); - assert(store.known_votes[0].checkpoint.slot == block_two.slot); - assert(roots_equal(&store.known_votes[0].checkpoint.root, &block_two_root)); + assert(!store.known_votes[0].has_checkpoint); assert(lantern_fork_choice_current_head(&store, &head) == 0); - assert(roots_equal(&head, &block_two_root)); + assert(roots_equal(&head, &genesis_root)); + lantern_store_reset(&backing_store); lantern_fork_choice_reset(&store); reset_block(&block_two); reset_block(&block_one); @@ -274,6 +266,88 @@ static int test_fork_choice_block_updates_checkpoints(void) { return 0; } +static int test_fork_choice_caches_block_states(void) { + LanternForkChoice store; + LanternStore backing_store; + lantern_fork_choice_init(&store); + lantern_store_init(&backing_store); + lantern_store_attach_fork_choice(&backing_store, &store); + + LanternConfig config = {.num_validators = 2, .genesis_time = 77}; + assert(lantern_store_prepare_fork_choice_votes(&backing_store, config.num_validators) == 0); + assert(lantern_fork_choice_configure(&store, &config) == 0); + + LanternBlock genesis; + init_block(&genesis, 0, 0, NULL, 0x51); + LanternRoot genesis_root; + assert(lantern_hash_tree_root_block(&genesis, &genesis_root) == 0); + LanternCheckpoint genesis_cp = make_checkpoint(&genesis_root, genesis.slot); + + LanternState genesis_state; + lantern_state_init(&genesis_state); + assert(lantern_state_generate_genesis(&genesis_state, config.genesis_time, config.num_validators) == 0); + genesis_state.latest_justified = genesis_cp; + genesis_state.latest_finalized = genesis_cp; + + assert( + lantern_fork_choice_set_anchor_with_state( + &store, + &genesis, + &genesis_cp, + &genesis_cp, + &genesis_root, + &genesis_state) + == 0); + + const LanternState *cached_genesis = + lantern_fork_choice_block_state(&store, &genesis_root); + assert(cached_genesis != NULL); + assert(cached_genesis->slot == genesis_state.slot); + assert(checkpoints_equal(&cached_genesis->latest_justified, &genesis_cp)); + assert(checkpoints_equal(&cached_genesis->latest_finalized, &genesis_cp)); + + LanternBlock child; + init_block(&child, 1, 1, &genesis_root, 0x52); + LanternRoot child_root; + assert(lantern_hash_tree_root_block(&child, &child_root) == 0); + LanternCheckpoint child_cp = make_checkpoint(&child_root, child.slot); + + LanternState child_state; + lantern_state_init(&child_state); + assert(lantern_state_clone(&genesis_state, &child_state) == 0); + child_state.slot = child.slot; + child_state.latest_block_header.slot = child.slot; + child_state.latest_block_header.proposer_index = child.proposer_index; + child_state.latest_block_header.parent_root = child.parent_root; + child_state.latest_justified = child_cp; + child_state.latest_finalized = genesis_cp; + + assert( + lantern_fork_choice_add_block_with_state( + &store, + &child, + NULL, + &child_state.latest_justified, + &child_state.latest_finalized, + &child_root, + &child_state) + == 0); + + const LanternState *cached_child = lantern_fork_choice_block_state(&store, &child_root); + assert(cached_child != NULL); + assert(cached_child->slot == child_state.slot); + assert(checkpoints_equal(&cached_child->latest_justified, &child_cp)); + assert(checkpoints_equal(&cached_child->latest_finalized, &genesis_cp)); + + lantern_state_reset(&child_state); + lantern_state_reset(&genesis_state); + lantern_store_reset(&backing_store); + lantern_fork_choice_reset(&store); + reset_block(&child); + reset_block(&genesis); + return 0; +} + static int test_fork_choice_vote_flow(void) { LanternForkChoice store; lantern_fork_choice_init(&store); @@ -365,6 +439,54 @@ static int test_fork_choice_vote_flow(void) { return 0; } +static int test_fork_choice_safe_target_merges_known_and_new_votes(void) { + LanternForkChoice store; + lantern_fork_choice_init(&store); + + LanternConfig config = {.num_validators = 3, .genesis_time = 25}; + assert(lantern_fork_choice_configure(&store, &config) == 0); + + LanternBlock genesis; + init_block(&genesis, 0, 0, NULL, 0x41); + LanternRoot genesis_root; + assert(lantern_hash_tree_root_block(&genesis, &genesis_root) == 0); + LanternCheckpoint genesis_cp = make_checkpoint(&genesis_root, genesis.slot); + assert(lantern_fork_choice_set_anchor(&store, &genesis, &genesis_cp, &genesis_cp, &genesis_root) == 0); + + LanternBlock block_one; + init_block(&block_one, 1, 0, &genesis_root, 0x42); + LanternRoot block_one_root; + assert(lantern_hash_tree_root_block(&block_one, &block_one_root) == 0); + assert( + lantern_fork_choice_add_block( + &store, + &block_one, + NULL, + NULL, + NULL, + &block_one_root) + == 0); + + LanternCheckpoint block_one_cp = make_checkpoint(&block_one_root, block_one.slot); + LanternSignedVote known_vote = make_vote(0, &genesis_cp, &block_one_cp); + assert(lantern_fork_choice_add_vote(&store, &known_vote, false) == 0); + assert(lantern_fork_choice_accept_new_votes(&store) == 0); + assert(store.known_votes[0].has_checkpoint); + + LanternSignedVote new_vote = make_vote(1, &genesis_cp, &block_one_cp); + assert(lantern_fork_choice_add_vote(&store, &new_vote, false) == 0); + assert(store.new_votes[1].has_checkpoint); + + assert(lantern_fork_choice_update_safe_target(&store) == 0); + const LanternRoot *safe_target = lantern_fork_choice_safe_target(&store); + assert(safe_target && roots_equal(safe_target, &block_one_root)); + + lantern_fork_choice_reset(&store); + reset_block(&block_one); + reset_block(&genesis); + return 0; +} + static int test_fork_choice_gossip_vote_dealiases_tables(void) { LanternForkChoice store; lantern_fork_choice_init(&store); @@ -655,11 +777,15 @@ static int test_fork_choice_advance_time_schedules_votes(void) { return 0; } -static int test_fork_choice_add_block_is_atomic_on_failure(void) { +static int test_fork_choice_add_block_ignores_invalid_proposer_vote(void) { LanternForkChoice store; + LanternStore backing_store; lantern_fork_choice_init(&store); + lantern_store_init(&backing_store); + lantern_store_attach_fork_choice(&backing_store, &store); LanternConfig config = {.num_validators = 2, .genesis_time = 100}; + assert(lantern_store_prepare_fork_choice_votes(&backing_store, config.num_validators) == 0); assert(lantern_fork_choice_configure(&store, &config) == 0); LanternBlock genesis; @@ -693,18 +819,26 @@ static int test_fork_choice_add_block_is_atomic_on_failure(void) { bad_proposer_vote.data.target = make_checkpoint(&parent_root, parent.slot); bad_proposer_vote.data.head = bad_proposer_vote.data.target; - assert(lantern_fork_choice_add_block(&store, &parent, &bad_proposer_vote, NULL, NULL, &parent_root) != 0); + assert(lantern_fork_choice_add_block(&store, &parent, &bad_proposer_vote, NULL, NULL, &parent_root) == 0); uint64_t parent_slot = 0; - assert(lantern_fork_choice_block_info(&store, &parent_root, &parent_slot, NULL, NULL) != 0); + bool parent_has_parent = true; + assert(lantern_fork_choice_block_info(&store, &parent_root, &parent_slot, NULL, &parent_has_parent) == 0); + assert(parent_slot == parent.slot); + assert(parent_has_parent == true); assert(lantern_fork_choice_block_info(&store, &child_root, &child_slot, NULL, &child_has_parent) == 0); - assert(child_has_parent == false); + assert(child_has_parent == true); LanternRoot head; assert(lantern_fork_choice_current_head(&store, &head) == 0); assert(roots_equal(&head, &genesis_root)); + assert(!store.new_votes[0].has_checkpoint); + assert(!store.new_votes[1].has_checkpoint); + assert(!store.known_votes[0].has_checkpoint); + assert(!store.known_votes[1].has_checkpoint); + lantern_store_reset(&backing_store); lantern_fork_choice_reset(&store); reset_block(&child); reset_block(&parent); @@ -712,6 +846,156 @@ static int test_fork_choice_add_block_is_atomic_on_failure(void) { return 0; } +static int test_fork_choice_add_block_skips_conflicting_block_attestation(void) { + LanternForkChoice store; + LanternStore backing_store; + LanternConfig config = {.num_validators = 1, .genesis_time = 120}; + LanternBlock genesis; + LanternBlock block_one; + LanternBlock block_two_a; + LanternBlock block_two_b; + LanternBlock block_three; + LanternRoot genesis_root; + LanternRoot block_one_root; + LanternRoot block_two_a_root; + LanternRoot block_two_b_root; + LanternRoot block_three_root; + LanternCheckpoint genesis_cp; + LanternCheckpoint block_one_cp; + LanternCheckpoint block_two_a_cp; + LanternCheckpoint block_two_b_cp; + LanternSignedVote known_vote; + LanternAttestations votes; + int rc = 1; + + memset(&store, 0, sizeof(store)); + memset(&backing_store, 0, sizeof(backing_store)); + memset(&genesis, 0, sizeof(genesis)); + memset(&block_one, 0, sizeof(block_one)); + memset(&block_two_a, 0, sizeof(block_two_a)); + memset(&block_two_b, 0, sizeof(block_two_b)); + memset(&block_three, 0, sizeof(block_three)); + memset(&genesis_root, 0, sizeof(genesis_root)); + memset(&block_one_root, 0, sizeof(block_one_root)); + memset(&block_two_a_root, 0, sizeof(block_two_a_root)); + memset(&block_two_b_root, 0, sizeof(block_two_b_root)); + memset(&block_three_root, 0, sizeof(block_three_root)); + lantern_attestations_init(&votes); + lantern_store_init(&backing_store); + lantern_fork_choice_init(&store); + lantern_store_attach_fork_choice(&backing_store, &store); + + if (lantern_store_prepare_fork_choice_votes(&backing_store, config.num_validators) != 0) { + fprintf(stderr, "failed to prepare fork choice vote tables in conflict test\n"); + goto cleanup; + } + + if (lantern_fork_choice_configure(&store, &config) != 0) { + fprintf(stderr, "failed to configure fork choice conflict test\n"); + goto cleanup; + } + + init_block(&genesis, 0, 0, NULL, 0x10); + if (lantern_hash_tree_root_block(&genesis, &genesis_root) != 0) { + fprintf(stderr, "failed to hash genesis block in conflict test\n"); + goto cleanup; + } + genesis_cp = make_checkpoint(&genesis_root, genesis.slot); + if (lantern_fork_choice_set_anchor(&store, &genesis, &genesis_cp, &genesis_cp, &genesis_root) != 0) { + fprintf(stderr, "failed to set anchor in conflict test\n"); + goto cleanup; + } + + init_block(&block_one, 1, 0, &genesis_root, 0x11); + if (lantern_hash_tree_root_block(&block_one, &block_one_root) != 0) { + fprintf(stderr, "failed to hash block one in conflict test\n"); + goto cleanup; + } + block_one_cp = make_checkpoint(&block_one_root, block_one.slot); + if (lantern_fork_choice_add_block(&store, &block_one, NULL, NULL, NULL, &block_one_root) != 0) { + fprintf(stderr, "failed to add block one in conflict test\n"); + goto cleanup; + } + + init_block(&block_two_a, 2, 0, &block_one_root, 0x12); + if (lantern_hash_tree_root_block(&block_two_a, &block_two_a_root) != 0) { + fprintf(stderr, "failed to hash block two A in conflict test\n"); + goto cleanup; + } + block_two_a_cp = make_checkpoint(&block_two_a_root, block_two_a.slot); + if (lantern_fork_choice_add_block(&store, &block_two_a, NULL, NULL, NULL, &block_two_a_root) != 0) { + fprintf(stderr, "failed to add block two A in conflict test\n"); + goto cleanup; + } + + init_block(&block_two_b, 2, 0, &block_one_root, 0x13); + if (lantern_hash_tree_root_block(&block_two_b, &block_two_b_root) != 0) { + fprintf(stderr, "failed to hash block two B in conflict test\n"); + goto cleanup; + } + block_two_b_cp = make_checkpoint(&block_two_b_root, block_two_b.slot); + if (lantern_fork_choice_add_block(&store, &block_two_b, NULL, NULL, NULL, &block_two_b_root) != 0) { + fprintf(stderr, "failed to add block two B in conflict test\n"); + goto cleanup; + } + + known_vote = make_vote(0, &block_one_cp, &block_two_a_cp); + if (lantern_fork_choice_add_vote(&store, &known_vote, true) != 0) { + fprintf(stderr, "failed to seed known vote in conflict test\n"); + goto cleanup; + } + if (!store.known_votes[0].has_checkpoint + || store.known_votes[0].checkpoint.slot != block_two_a.slot + || !roots_equal(&store.known_votes[0].checkpoint.root, &block_two_a_root)) { + fprintf(stderr, "known vote seed mismatch in conflict test\n"); + goto cleanup; + } + + init_block(&block_three, 3, 0, &block_two_a_root, 0x14); + if (lantern_hash_tree_root_block(&block_three, &block_three_root) != 0) { + fprintf(stderr, "failed to hash block three in conflict test\n"); + goto cleanup; + } + + if (lantern_attestations_resize(&votes, 1u) != 0 || !votes.data) { + fprintf(stderr, "failed to allocate conflicting attestation list\n"); + goto cleanup; + } + votes.data[0] = make_vote(0, &block_one_cp, &block_two_b_cp).data; + if (lantern_wrap_attestations_as_aggregated(&votes, &block_three.body.attestations) != 0) { + fprintf(stderr, "failed to build conflicting aggregated attestation\n"); + goto cleanup; + } + + if (lantern_fork_choice_add_block(&store, &block_three, NULL, NULL, NULL, &block_three_root) != 0) { + fprintf(stderr, "block import failed when conflicting block attestation should be skipped\n"); + goto cleanup; + } + if (!store.known_votes[0].has_checkpoint + || store.known_votes[0].checkpoint.slot != block_two_a.slot + || !roots_equal(&store.known_votes[0].checkpoint.root, &block_two_a_root)) { + fprintf(stderr, "known vote changed after skipping conflicting block attestation\n"); + goto cleanup; + } + if (lantern_fork_choice_block_info(&store, &block_three_root, NULL, NULL, NULL) != 0) { + fprintf(stderr, "block missing after skipping conflicting block attestation\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + lantern_attestations_reset(&votes); + lantern_store_reset(&backing_store); + lantern_fork_choice_reset(&store); + reset_block(&block_three); + reset_block(&block_two_b); + reset_block(&block_two_a); + reset_block(&block_one); + reset_block(&genesis); + return rc; +} + int main(void) { if (test_fork_choice_proposer_attestation_sequence() != 0) { return 1; @@ -719,9 +1003,15 @@ int main(void) { if (test_fork_choice_block_updates_checkpoints() != 0) { return 1; } + if (test_fork_choice_caches_block_states() != 0) { + return 1; + } if (test_fork_choice_vote_flow() != 0) { return 1; } + if (test_fork_choice_safe_target_merges_known_and_new_votes() != 0) { + return 1; + } if (test_fork_choice_gossip_vote_dealiases_tables() != 0) { return 1; } @@ -734,7 +1024,10 @@ int main(void) { if (test_fork_choice_advance_time_schedules_votes() != 0) { return 1; } - if (test_fork_choice_add_block_is_atomic_on_failure() != 0) { + if (test_fork_choice_add_block_ignores_invalid_proposer_vote() != 0) { + return 1; + } + if (test_fork_choice_add_block_skips_conflicting_block_attestation() != 0) { return 1; } return 0; diff --git a/tests/unit/test_off_head_replay.c b/tests/unit/test_off_head_replay.c index 01d8e44..d9bee41 100644 --- a/tests/unit/test_off_head_replay.c +++ b/tests/unit/test_off_head_replay.c @@ -13,16 +13,9 @@ #include "lantern/consensus/ssz.h" #include "lantern/storage/storage.h" #include "lantern/support/strings.h" +#include "../support/state_store_adapter.h" #define DEVNET_DATA_DIR "internal-docs/pending-issues/devnet-run-data/lantern_0" -#define LANTERN_STORAGE_STATE_META_VERSION 1u - -struct state_meta_wire { - uint32_t version; - uint32_t reserved; - uint64_t historical_roots_offset; - uint64_t justified_slots_offset; -}; struct head_record_wire { uint64_t slot; @@ -132,29 +125,6 @@ static int root_to_hex(const LanternRoot *root, char *out, size_t out_len, bool return 0; } -static int read_meta_file(const char *path, struct state_meta_wire *out_meta) { - if (!path || !out_meta) { - return -1; - } - uint8_t *data = NULL; - size_t len = 0; - int rc = read_file_bytes(path, &data, &len); - if (rc != 0) { - free(data); - return rc; - } - if (len != sizeof(*out_meta)) { - free(data); - return -1; - } - memcpy(out_meta, data, sizeof(*out_meta)); - free(data); - if (out_meta->version != LANTERN_STORAGE_STATE_META_VERSION) { - return -1; - } - return 0; -} - static int load_block_by_root_filename( const char *data_dir, const LanternRoot *root, @@ -195,7 +165,7 @@ static int load_block_by_root_filename( /* * This intentionally mirrors the buggy fast path in rebuild_state_for_root_locked: - * decode SSZ snapshot bytes directly without state meta restoration or vote buffer prep. + * decode SSZ snapshot bytes directly without vote buffer prep. */ static int load_snapshot_buggy( const char *data_dir, @@ -223,7 +193,6 @@ static int load_snapshot_buggy( /* * Fixed reconstruction path for diagnostics: * - decode SSZ snapshot - * - restore per-snapshot offsets from states/.meta when present * - allocate validator_votes backing storage expected by attestation processing */ static int load_snapshot_fixed( @@ -234,29 +203,6 @@ static int load_snapshot_fixed( return -1; } - char root_hex[2u * LANTERN_ROOT_SIZE + 1u]; - if (root_to_hex(state_root, root_hex, sizeof(root_hex), false) != 0) { - lantern_state_reset(out_state); - return -1; - } - - char meta_path[PATH_MAX]; - int path_written = snprintf(meta_path, sizeof(meta_path), "%s/states/%s.meta", data_dir, root_hex); - if (path_written < 0 || (size_t)path_written >= sizeof(meta_path)) { - lantern_state_reset(out_state); - return -1; - } - - struct state_meta_wire meta = {0}; - int meta_rc = read_meta_file(meta_path, &meta); - if (meta_rc == 0) { - out_state->historical_roots_offset = meta.historical_roots_offset; - out_state->justified_slots_offset = meta.justified_slots_offset; - } else if (meta_rc < 0) { - lantern_state_reset(out_state); - return -1; - } - if (out_state->config.num_validators == 0) { lantern_state_reset(out_state); return -1; @@ -273,17 +219,15 @@ static void print_state_summary(const char *prefix, const LanternState *state) { if (!prefix || !state) { return; } + const LanternStore *store = lantern_test_state_store_find(state); printf( "%s slot=%" PRIu64 " validators=%" PRIu64 - " validator_votes_ptr=%p validator_votes_len=%zu" - " hist_offset=%" PRIu64 " just_offset=%" PRIu64 "\n", + " validator_votes_ptr=%p validator_votes_len=%zu\n", prefix, state->slot, state->config.num_validators, - (void *)state->validator_votes, - state->validator_votes_len, - state->historical_roots_offset, - state->justified_slots_offset); + store ? (void *)store->validator_votes : NULL, + store ? store->validator_votes_len : 0u); } static int diagnose_transition_failure( @@ -346,16 +290,16 @@ static int diagnose_transition_failure( } int att_rc = lantern_state_process_attestations(&state, &expanded, NULL); + const LanternStore *store = lantern_test_state_store_find(&state); printf("diagnostic: lantern_state_process_attestations rc=%d\n", att_rc); printf( "diagnostic: validator_votes_ptr=%p validator_votes_len=%zu expected=%" PRIu64 "\n", - (void *)state.validator_votes, - state.validator_votes_len, + store ? (void *)store->validator_votes : NULL, + store ? store->validator_votes_len : 0u, state.config.num_validators); if (att_rc != 0 - && (!state.validator_votes - || state.validator_votes_len != (size_t)state.config.num_validators)) { + && (!store || !store->validator_votes || store->validator_votes_len != (size_t)state.config.num_validators)) { *out_hit_expected_fail_condition = true; printf( "FAIL_POINT: state.c rejects attestation processing because " @@ -490,26 +434,6 @@ int main(void) { print_state_summary("persisted state", &persisted); lantern_state_reset(&persisted); - char state_meta_path[PATH_MAX]; - int meta_written = snprintf(state_meta_path, sizeof(state_meta_path), "%s/state.meta", DEVNET_DATA_DIR); - if (meta_written < 0 || (size_t)meta_written >= sizeof(state_meta_path)) { - fprintf(stderr, "failed to build state.meta path\n"); - return EXIT_FAILURE; - } - - struct state_meta_wire state_meta = {0}; - int state_meta_rc = read_meta_file(state_meta_path, &state_meta); - if (state_meta_rc == 0) { - printf( - "state.meta version=%u historical_roots_offset=%" PRIu64 " justified_slots_offset=%" PRIu64 "\n", - state_meta.version, - state_meta.historical_roots_offset, - state_meta.justified_slots_offset); - } else { - fprintf(stderr, "failed to read %s rc=%d\n", state_meta_path, state_meta_rc); - return EXIT_FAILURE; - } - const struct expected_failure_block failing_blocks[] = { { .root_hex = "c3408383ca7fe5462b9edc7a48ec38d0603af497e7c597455529820423d63d2d", diff --git a/tests/unit/test_state.c b/tests/unit/test_state.c index df40881..d8634aa 100644 --- a/tests/unit/test_state.c +++ b/tests/unit/test_state.c @@ -10,7 +10,10 @@ #include "lantern/consensus/hash.h" #include "lantern/consensus/fork_choice.h" #include "lantern/consensus/quorum.h" +#include "lantern/consensus/signature.h" #include "lantern/consensus/state.h" +#include "pq-bindings-c-rust.h" +#include "../support/state_store_adapter.h" static void expect_zero(int rc, const char *label) { if (rc != 0) { @@ -47,6 +50,87 @@ static void fill_signature(LanternSignature *signature, uint8_t value) { memset(signature->bytes, value, LANTERN_SIGNATURE_SIZE); } +static int generate_test_keypair( + struct PQSignatureSchemePublicKey **out_pub, + struct PQSignatureSchemeSecretKey **out_secret) { + if (!out_pub || !out_secret) { + return -1; + } + *out_pub = NULL; + *out_secret = NULL; + enum PQSigningError err = pq_key_gen(0, 4u, out_pub, out_secret); + if (err != Success || !*out_pub || !*out_secret) { + if (*out_pub) { + pq_public_key_free(*out_pub); + *out_pub = NULL; + } + if (*out_secret) { + pq_secret_key_free(*out_secret); + *out_secret = NULL; + } + return -1; + } + return 0; +} + +static int set_test_validator_pubkey( + LanternState *state, + size_t validator_count, + size_t validator_index, + struct PQSignatureSchemePublicKey *pubkey) { + if (!state || !pubkey || validator_count == 0 || validator_index >= validator_count) { + return -1; + } + + uint8_t serialized_pubkey[LANTERN_VALIDATOR_PUBKEY_SIZE]; + uintptr_t written = 0; + enum PQSigningError err = pq_public_key_serialize( + pubkey, + serialized_pubkey, + sizeof(serialized_pubkey), + &written); + if (err != Success || written != LANTERN_VALIDATOR_PUBKEY_SIZE) { + return -1; + } + + uint8_t *pubkeys = calloc(validator_count, LANTERN_VALIDATOR_PUBKEY_SIZE); + if (!pubkeys) { + return -1; + } + memcpy( + pubkeys + (validator_index * LANTERN_VALIDATOR_PUBKEY_SIZE), + serialized_pubkey, + LANTERN_VALIDATOR_PUBKEY_SIZE); + int rc = lantern_state_set_validator_pubkeys(state, pubkeys, validator_count); + free(pubkeys); + return rc; +} + +static int sign_vote_with_secret( + LanternSignedVote *vote, + struct PQSignatureSchemeSecretKey *secret) { + if (!vote || !secret) { + return -1; + } + LanternRoot vote_root; + if (lantern_hash_tree_root_attestation_data(&vote->data.data, &vote_root) != 0) { + return -1; + } + if (!lantern_signature_sign(secret, vote->data.slot, &vote_root, &vote->signature)) { + return -1; + } + return 0; +} + +static void build_vote( + LanternVote *out_vote, + LanternSignature *out_signature, + uint64_t validator_id, + uint64_t slot, + const LanternCheckpoint *source, + const LanternCheckpoint *target_template, + uint8_t head_marker); + static void zero_root(LanternRoot *root) { if (!root) { return; @@ -64,16 +148,23 @@ static bool bitlist_test_bit(const struct lantern_bitlist *bitlist, size_t index return (bitlist->bytes[byte_index] & (uint8_t)(1u << bit_index)) != 0; } +static uint64_t justified_slots_anchor_for_tests(const LanternState *state) { + assert(state != NULL); + assert(state->latest_finalized.slot != UINT64_MAX); + return state->latest_finalized.slot + 1u; +} + static void mark_slot_justified_for_tests(LanternState *state, uint64_t slot) { if (!state) { return; } - /* Slots before the offset are already considered finalized/justified */ - if (slot < state->justified_slots_offset) { + uint64_t anchor = justified_slots_anchor_for_tests(state); + /* Slots before the anchor are already considered finalized/justified. */ + if (slot < anchor) { return; } - /* Calculate the relative index from the offset */ - size_t index = (size_t)(slot - state->justified_slots_offset); + /* Calculate the relative index from the finalized-slot anchor. */ + size_t index = (size_t)(slot - anchor); expect_zero( lantern_bitlist_resize(&state->justified_slots, index + 1), "resize justified slots for test"); @@ -160,6 +251,12 @@ static void setup_state_and_fork_choice( expect_zero(lantern_state_generate_genesis(state, genesis_time, validator_count), "generate genesis for setup"); lantern_fork_choice_init(fork_choice); + lantern_state_attach_fork_choice(state, fork_choice); + expect_zero( + lantern_store_prepare_fork_choice_votes( + lantern_test_state_store_ensure(state), + validator_count), + "prepare fork choice votes for setup"); expect_zero(lantern_fork_choice_configure(fork_choice, &state->config), "configure fork choice for setup"); LanternRoot state_root; @@ -421,7 +518,7 @@ static int test_process_slots_rejects_non_future_target(void) { static int test_state_transition_applies_block(void) { const uint64_t genesis_time = 500; - const uint64_t validator_count = 8; + const uint64_t validator_count = 1; LanternState state; lantern_state_init(&state); @@ -431,6 +528,16 @@ static int test_state_transition_applies_block(void) { lantern_state_init(&expected); expect_zero(lantern_state_generate_genesis(&expected, genesis_time, validator_count), "generate expected state"); + struct PQSignatureSchemePublicKey *proposer_pub = NULL; + struct PQSignatureSchemeSecretKey *proposer_secret = NULL; + expect_zero(generate_test_keypair(&proposer_pub, &proposer_secret), "generate proposer keypair"); + expect_zero( + set_test_validator_pubkey(&state, (size_t)validator_count, 0u, proposer_pub), + "set proposer pubkey on state"); + expect_zero( + set_test_validator_pubkey(&expected, (size_t)validator_count, 0u, proposer_pub), + "set proposer pubkey on expected state"); + LanternBlock block; memset(&block, 0, sizeof(block)); block.slot = 1; @@ -449,6 +556,23 @@ static int test_state_transition_applies_block(void) { LanternSignedBlock signed_block; memset(&signed_block, 0, sizeof(signed_block)); signed_block.message.block = block; + LanternCheckpoint proposer_source = {.slot = 0u}; + LanternCheckpoint proposer_target = {.slot = block.slot}; + fill_root(&proposer_source.root, 0x11); + fill_root(&proposer_target.root, 0x22); + LanternSignedVote proposer_vote; + memset(&proposer_vote, 0, sizeof(proposer_vote)); + build_vote( + &proposer_vote.data, + NULL, + block.proposer_index, + block.slot, + &proposer_source, + &proposer_target, + 0x33); + expect_zero(sign_vote_with_secret(&proposer_vote, proposer_secret), "sign proposer vote"); + signed_block.message.proposer_attestation = proposer_vote.data; + signed_block.signatures.proposer_signature = proposer_vote.signature; expect_zero(lantern_state_transition(&state, &signed_block), "state transition"); LanternRoot post_root; @@ -459,11 +583,99 @@ static int test_state_transition_applies_block(void) { assert(state.historical_block_hashes.length == expected.historical_block_hashes.length); lantern_block_body_reset(&block.body); + pq_secret_key_free(proposer_secret); + pq_public_key_free(proposer_pub); lantern_state_reset(&state); lantern_state_reset(&expected); return 0; } +static int test_state_transition_rejects_missing_proposer_signature(void) { + const uint64_t genesis_time = 501; + const uint64_t validator_count = 1; + + LanternState state; + lantern_state_init(&state); + expect_zero(lantern_state_generate_genesis(&state, genesis_time, validator_count), "generate signed state"); + + struct PQSignatureSchemePublicKey *proposer_pub = NULL; + struct PQSignatureSchemeSecretKey *proposer_secret = NULL; + expect_zero(generate_test_keypair(&proposer_pub, &proposer_secret), "generate missing-signature keypair"); + expect_zero( + set_test_validator_pubkey(&state, (size_t)validator_count, 0u, proposer_pub), + "set missing-signature pubkey"); + + LanternState expected; + lantern_state_init(&expected); + expect_zero(lantern_state_generate_genesis(&expected, genesis_time, validator_count), "generate expected signed state"); + expect_zero( + set_test_validator_pubkey(&expected, (size_t)validator_count, 0u, proposer_pub), + "set expected missing-signature pubkey"); + + LanternBlock block; + memset(&block, 0, sizeof(block)); + block.slot = 1; + expect_zero(lantern_proposer_for_slot(block.slot, validator_count, &block.proposer_index), "compute missing-signature proposer"); + lantern_block_body_init(&block.body); + + expect_zero(lantern_state_process_slots(&expected, block.slot), "advance expected signed slots"); + LanternRoot parent_root; + expect_zero( + lantern_hash_tree_root_block_header(&expected.latest_block_header, &parent_root), + "hash missing-signature parent"); + block.parent_root = parent_root; + LanternRoot expected_state_root; + expect_zero(lantern_state_process_block(&expected, &block, NULL, NULL), "apply expected unsigned block"); + expect_zero(lantern_hash_tree_root_state(&expected, &expected_state_root), "hash expected unsigned post state"); + block.state_root = expected_state_root; + + LanternSignedBlock signed_block; + memset(&signed_block, 0, sizeof(signed_block)); + signed_block.message.block = block; + LanternCheckpoint proposer_source = {.slot = 0u}; + LanternCheckpoint proposer_target = {.slot = block.slot}; + fill_root(&proposer_source.root, 0x41); + fill_root(&proposer_target.root, 0x52); + LanternSignedVote proposer_vote; + memset(&proposer_vote, 0, sizeof(proposer_vote)); + build_vote( + &proposer_vote.data, + NULL, + block.proposer_index, + block.slot, + &proposer_source, + &proposer_target, + 0x63); + expect_zero(sign_vote_with_secret(&proposer_vote, proposer_secret), "sign missing-signature proposer vote"); + signed_block.message.proposer_attestation = proposer_vote.data; + + if (lantern_state_transition(&state, &signed_block) == 0) { + fprintf(stderr, "state transition accepted block without proposer signature\n"); + lantern_block_body_reset(&block.body); + pq_secret_key_free(proposer_secret); + pq_public_key_free(proposer_pub); + lantern_state_reset(&expected); + lantern_state_reset(&state); + return 1; + } + if (state.slot != 0u) { + fprintf(stderr, "state transition mutated slot on signature failure (slot=%" PRIu64 ")\n", state.slot); + lantern_block_body_reset(&block.body); + pq_secret_key_free(proposer_secret); + pq_public_key_free(proposer_pub); + lantern_state_reset(&expected); + lantern_state_reset(&state); + return 1; + } + + lantern_block_body_reset(&block.body); + pq_secret_key_free(proposer_secret); + pq_public_key_free(proposer_pub); + lantern_state_reset(&expected); + lantern_state_reset(&state); + return 0; +} + static int test_state_transition_rejects_genesis_state_root_mismatch(void) { const uint64_t genesis_time = 600; const uint64_t validator_count = 4; @@ -559,6 +771,64 @@ static int append_aggregated_attestation_from_vote( return rc; } +static int build_cached_proof_for_vote( + LanternAggregatedSignatureProof *out_proof, + uint64_t validator_id, + uint8_t marker) { + if (!out_proof || validator_id >= LANTERN_VALIDATOR_REGISTRY_LIMIT) { + return -1; + } + lantern_aggregated_signature_proof_init(out_proof); + size_t bit_length = (size_t)validator_id + 1u; + if (lantern_bitlist_resize(&out_proof->participants, bit_length) != 0) { + lantern_aggregated_signature_proof_reset(out_proof); + return -1; + } + if (lantern_bitlist_set(&out_proof->participants, (size_t)validator_id, true) != 0) { + lantern_aggregated_signature_proof_reset(out_proof); + return -1; + } + if (lantern_byte_list_resize(&out_proof->proof_data, 8u) != 0) { + lantern_aggregated_signature_proof_reset(out_proof); + return -1; + } + for (size_t i = 0; i < out_proof->proof_data.length; ++i) { + out_proof->proof_data.data[i] = (uint8_t)(marker + (uint8_t)i); + } + return 0; +} + +static int seed_known_payload_for_vote( + LanternState *state, + const LanternVote *vote, + uint8_t marker) { + if (!state || !vote) { + return -1; + } + LanternStore *store = lantern_test_state_store_ensure(state); + if (!store) { + return -1; + } + + LanternRoot data_root; + if (lantern_hash_tree_root_attestation_data(&vote->data, &data_root) != 0) { + return -1; + } + + LanternAggregatedSignatureProof proof; + if (build_cached_proof_for_vote(&proof, vote->validator_id, marker) != 0) { + return -1; + } + int rc = lantern_store_add_known_aggregated_payload( + store, + &data_root, + &vote->data, + &proof, + vote->target.slot); + lantern_aggregated_signature_proof_reset(&proof); + return rc; +} + static const LanternVote *find_vote_by_validator(const LanternAttestations *attestations, uint64_t validator_id) { if (!attestations) { return NULL; @@ -1002,7 +1272,7 @@ static int test_pending_votes_survive_interleaved_justification_and_finalization const uint64_t target_justified_slot = 343u; const uint64_t target_pending_slot = 350u; const uint64_t block_slot = 354u; - const uint64_t offset = finalized_slot + 1u; + const uint64_t anchor = finalized_slot + 1u; const size_t validator_count_sz = (size_t)validator_count; populate_historical_hashes_for_tests(&state, target_pending_slot); @@ -1012,9 +1282,8 @@ static int test_pending_votes_survive_interleaved_justification_and_finalization state.latest_justified.slot = justified_slot; state.latest_justified.root = get_historical_root_for_tests(&state, justified_slot); - state.justified_slots_offset = offset; expect_zero( - lantern_bitlist_resize(&state.justified_slots, (size_t)(target_pending_slot - offset + 1u)), + lantern_bitlist_resize(&state.justified_slots, (size_t)(target_pending_slot - anchor + 1u)), "resize justified slots for interleaved pending votes test"); assert(state.justified_slots.bytes != NULL); memset(state.justified_slots.bytes, 0, state.justified_slots.capacity); @@ -1155,7 +1424,7 @@ static int test_pending_votes_preserved_when_new_root_inserts_before_existing_ro const uint64_t justified_slot = 340u; const uint64_t target_justified_slot = 343u; const uint64_t target_pending_slot = 350u; - const uint64_t offset = finalized_slot + 1u; + const uint64_t anchor = finalized_slot + 1u; const size_t validator_count_sz = (size_t)validator_count; populate_historical_hashes_for_tests(&state, target_pending_slot); @@ -1165,9 +1434,8 @@ static int test_pending_votes_preserved_when_new_root_inserts_before_existing_ro state.latest_justified.slot = justified_slot; state.latest_justified.root = get_historical_root_for_tests(&state, justified_slot); - state.justified_slots_offset = offset; expect_zero( - lantern_bitlist_resize(&state.justified_slots, (size_t)(target_pending_slot - offset + 1u)), + lantern_bitlist_resize(&state.justified_slots, (size_t)(target_pending_slot - anchor + 1u)), "resize justified slots for insertion-order pending votes test"); assert(state.justified_slots.bytes != NULL); memset(state.justified_slots.bytes, 0, state.justified_slots.capacity); @@ -1513,14 +1781,7 @@ static int test_collect_attestations_for_block(void) { build_vote(&input.data[0], &input_signatures.data[0], 0, target.slot, &justified, &target, 0x01); build_vote(&input.data[1], &input_signatures.data[1], 1, target.slot, &justified, &target, 0x02); - - LanternCheckpoint other_source = justified; - other_source.slot = justified.slot + 2; - fill_root(&other_source.root, 0xA0); - LanternCheckpoint other_target = other_source; - other_target.slot = other_source.slot + 1; - fill_root(&other_target.root, 0xB0); - build_vote(&input.data[2], &input_signatures.data[2], 2, other_target.slot, &other_source, &other_target, 0x03); + build_vote(&input.data[2], &input_signatures.data[2], 2, target.slot, &justified, &target, 0x03); LanternSignedVote signed_vote; memset(&signed_vote, 0, sizeof(signed_vote)); @@ -1532,7 +1793,9 @@ static int test_collect_attestations_for_block(void) { expect_zero(lantern_state_set_signed_validator_vote(&state, 1, &signed_vote), "store vote 1"); signed_vote.data = input.data[2]; signed_vote.signature = input_signatures.data[2]; - expect_zero(lantern_state_set_signed_validator_vote(&state, 2, &signed_vote), "store other vote"); + expect_zero(lantern_state_set_signed_validator_vote(&state, 2, &signed_vote), "store vote 2"); + expect_zero(seed_known_payload_for_vote(&state, &input.data[0], 0x51), "seed known payload 0"); + expect_zero(seed_known_payload_for_vote(&state, &input.data[1], 0x52), "seed known payload 1"); uint64_t block_slot = state.slot + 1u; uint64_t proposer_index = 0; @@ -1740,73 +2003,123 @@ static int test_process_attestations_preserves_signed_votes(void) { } static int test_process_block_defers_proposer_attestation(void) { - LanternState without_vote; - LanternState with_vote; - lantern_state_init(&without_vote); - lantern_state_init(&with_vote); + LanternState state; + LanternForkChoice fork_choice; + LanternRoot anchor_root; + lantern_state_init(&state); + lantern_fork_choice_init(&fork_choice); const uint64_t genesis_time = 777; const uint64_t validator_count = 1; + setup_state_and_fork_choice(&state, &fork_choice, genesis_time, validator_count, &anchor_root); + + struct PQSignatureSchemePublicKey *proposer_pub = NULL; + struct PQSignatureSchemeSecretKey *proposer_secret = NULL; + expect_zero(generate_test_keypair(&proposer_pub, &proposer_secret), "generate proposer vote keypair"); expect_zero( - lantern_state_generate_genesis(&without_vote, genesis_time, validator_count), - "genesis for proposer block (without vote)"); - expect_zero( - lantern_state_generate_genesis(&with_vote, genesis_time, validator_count), - "genesis for proposer block (with vote)"); - mark_slot_justified_for_tests(&without_vote, without_vote.latest_justified.slot); - mark_slot_justified_for_tests(&with_vote, with_vote.latest_justified.slot); + set_test_validator_pubkey(&state, (size_t)validator_count, 0u, proposer_pub), + "set proposer pubkey"); + mark_slot_justified_for_tests(&state, state.latest_justified.slot); /* Populate historical hashes so proposer attestation validation passes */ - populate_historical_hashes_for_tests(&without_vote, 1); - populate_historical_hashes_for_tests(&with_vote, 1); + populate_historical_hashes_for_tests(&state, 1); - LanternBlock block; - memset(&block, 0, sizeof(block)); - block.slot = without_vote.slot + 1u; + LanternSignedBlock signed_block; + memset(&signed_block, 0, sizeof(signed_block)); + LanternBlock *block = &signed_block.message.block; + block->slot = state.slot + 1u; expect_zero( - lantern_proposer_for_slot(block.slot, validator_count, &block.proposer_index), + lantern_proposer_for_slot(block->slot, validator_count, &block->proposer_index), "proposer for proposer vote test"); expect_zero( - lantern_state_select_block_parent(&without_vote, &block.parent_root), + lantern_state_select_block_parent(&state, &block->parent_root), "parent root for proposer vote test"); - lantern_block_body_init(&block.body); - LanternBlockSignatures block_sigs; - lantern_block_signatures_init(&block_sigs); + lantern_block_body_init(&block->body); /* Use roots from historical_block_hashes so attestations pass validation */ - LanternCheckpoint base = without_vote.latest_justified; - base.root = get_historical_root_for_tests(&without_vote, base.slot); + 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(&without_vote, next.slot); + next.root = get_historical_root_for_tests(&state, next.slot); LanternSignedVote proposer_vote; memset(&proposer_vote, 0, sizeof(proposer_vote)); - build_vote(&proposer_vote.data, &proposer_vote.signature, block.proposer_index, block.slot, &base, &next, 0xB2); + build_vote(&proposer_vote.data, NULL, block->proposer_index, block->slot, &base, &next, 0xB2); + 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; - expect_zero(lantern_state_process_slots(&without_vote, block.slot), "advance slots without proposer vote"); + LanternRoot expected_state_root; expect_zero( - lantern_state_process_block(&without_vote, &block, &block_sigs, NULL), - "process block without proposer vote"); - assert(without_vote.latest_justified.slot == base.slot); + lantern_state_preview_post_state_root(&state, &signed_block, &expected_state_root), + "preview proposer block state root"); + block->state_root = expected_state_root; + + expect_zero(lantern_state_transition(&state, &signed_block), "import block with proposer attestation"); + assert(state.latest_justified.slot == base.slot); + + if (lantern_state_validator_has_vote(&state, (size_t)block->proposer_index)) { + fprintf(stderr, "proposer attestation should not be staged into validator vote cache\n"); + lantern_block_body_reset(&block->body); + pq_secret_key_free(proposer_secret); + pq_public_key_free(proposer_pub); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } - expect_zero(lantern_state_process_slots(&with_vote, block.slot), "advance slots with proposer vote"); + LanternStore *store = lantern_test_state_store_ensure(&state); + if (!store) { + fprintf(stderr, "state store missing after proposer import\n"); + lantern_block_body_reset(&block->body); + pq_secret_key_free(proposer_secret); + pq_public_key_free(proposer_pub); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + + LanternRoot data_root; + expect_zero( + lantern_hash_tree_root_attestation_data(&proposer_vote.data.data, &data_root), + "hash proposer attestation data root"); + LanternSignatureKey key = { + .validator_index = proposer_vote.data.validator_id, + .data_root = data_root, + }; + LanternSignature cached_signature; + memset(&cached_signature, 0, sizeof(cached_signature)); expect_zero( - lantern_state_process_block(&with_vote, &block, &block_sigs, &proposer_vote), - "process block with proposer vote"); - assert(with_vote.latest_justified.slot == base.slot); + lantern_store_get_gossip_signature(store, &key, &cached_signature), + "retrieve cached proposer signature"); + assert(memcmp( + cached_signature.bytes, + signed_block.signatures.proposer_signature.bytes, + LANTERN_SIGNATURE_SIZE) + == 0); - LanternSignedVote stored_vote; - memset(&stored_vote, 0, sizeof(stored_vote)); + LanternAttestationData cached_data; + memset(&cached_data, 0, sizeof(cached_data)); expect_zero( - lantern_state_get_signed_validator_vote(&with_vote, (size_t)block.proposer_index, &stored_vote), - "retrieve staged proposer attestation"); - assert(checkpoints_equal(&stored_vote.data.source, &base)); - assert(checkpoints_equal(&stored_vote.data.target, &next)); + 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)); - lantern_block_body_reset(&block.body); - lantern_block_signatures_reset(&block_sigs); - lantern_state_reset(&without_vote); - lantern_state_reset(&with_vote); + assert(fork_choice.new_votes != NULL); + assert(fork_choice.known_votes != NULL); + assert(!fork_choice.new_votes[block->proposer_index].has_checkpoint); + assert(!fork_choice.known_votes[block->proposer_index].has_checkpoint); + + LanternRoot head; + expect_zero(lantern_fork_choice_current_head(&fork_choice, &head), "fork choice head after proposer import"); + assert(memcmp(head.bytes, anchor_root.bytes, LANTERN_ROOT_SIZE) == 0); + + lantern_block_body_reset(&block->body); + pq_secret_key_free(proposer_secret); + pq_public_key_free(proposer_pub); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); return 0; } @@ -1857,13 +2170,17 @@ static int test_collect_attestations_fixed_point(void) { /* Validators 0,1,2 vote for base→mid */ build_vote(&vote.data, &vote.signature, 0, mid.slot, &base, &mid, 0x21); expect_zero(lantern_state_set_signed_validator_vote(&state, 0, &vote), "store fixed vote 0"); + expect_zero(seed_known_payload_for_vote(&state, &vote.data, 0x61), "seed fixed payload 0"); build_vote(&vote.data, &vote.signature, 1, mid.slot, &base, &mid, 0x22); expect_zero(lantern_state_set_signed_validator_vote(&state, 1, &vote), "store fixed vote 1"); + expect_zero(seed_known_payload_for_vote(&state, &vote.data, 0x62), "seed fixed payload 1"); build_vote(&vote.data, &vote.signature, 2, mid.slot, &base, &mid, 0x23); expect_zero(lantern_state_set_signed_validator_vote(&state, 2, &vote), "store fixed vote 2"); + expect_zero(seed_known_payload_for_vote(&state, &vote.data, 0x63), "seed fixed payload 2"); /* Validator 3 votes for mid→tip (this won't reach quorum alone, but tests the fixed-point logic) */ build_vote(&vote.data, &vote.signature, 3, tip.slot, &mid, &tip, 0x24); expect_zero(lantern_state_set_signed_validator_vote(&state, 3, &vote), "store fixed vote 3"); + expect_zero(seed_known_payload_for_vote(&state, &vote.data, 0x64), "seed fixed payload 3"); uint64_t block_slot = state.slot + 1u; uint64_t proposer_index = 0; @@ -2005,6 +2322,9 @@ static int test_collect_attestations_fixed_point_deep_chain(void) { memset(&vote, 0, sizeof(vote)); build_vote(&vote.data, &vote.signature, i, target.slot, &base, &target, (uint8_t)(0x60u + i)); expect_zero(lantern_state_set_signed_validator_vote(&state, i, &vote), "store deep fixed vote"); + expect_zero( + seed_known_payload_for_vote(&state, &vote.data, (uint8_t)(0xA0u + (uint8_t)i)), + "seed deep fixed payload"); } uint64_t block_slot = state.slot + 1u; @@ -2091,6 +2411,12 @@ static int test_select_block_parent_uses_fork_choice(void) { LanternForkChoice fork_choice; lantern_fork_choice_init(&fork_choice); + lantern_state_attach_fork_choice(&state, &fork_choice); + expect_zero( + lantern_store_prepare_fork_choice_votes( + lantern_test_state_store_ensure(&state), + state.config.num_validators), + "prepare fork choice votes"); expect_zero(lantern_fork_choice_configure(&fork_choice, &state.config), "configure fork choice"); LanternRoot genesis_state_root; expect_zero(lantern_hash_tree_root_state(&state, &genesis_state_root), "hash genesis state root"); @@ -2619,6 +2945,7 @@ static int test_history_limits_enforced(void) { expect_zero( lantern_root_list_resize(&state.historical_block_hashes, LANTERN_HISTORICAL_ROOTS_LIMIT), "prep historical roots"); + fill_root(&state.historical_block_hashes.items[0], 0x5Au); expect_zero( lantern_bitlist_resize(&state.justified_slots, LANTERN_HISTORICAL_ROOTS_LIMIT), "prep justified slots"); @@ -2639,9 +2966,8 @@ static int test_history_limits_enforced(void) { expect_zero(lantern_state_process_block_header(&state, &block), "process history limit block"); assert(state.historical_block_hashes.length == LANTERN_HISTORICAL_ROOTS_LIMIT); - assert(state.historical_roots_offset == 1u); + assert(state.historical_block_hashes.items[0].bytes[0] == 0x5Au); assert(state.justified_slots.bit_length == LANTERN_HISTORICAL_ROOTS_LIMIT); - assert(state.justified_slots_offset == 1u); lantern_block_body_reset(&block.body); lantern_state_reset(&state); @@ -2653,25 +2979,26 @@ static int test_justified_slot_window_helpers(void) { lantern_state_init(&state); expect_zero(lantern_state_generate_genesis(&state, 111, 4), "genesis for slot window test"); + state.latest_finalized.slot = 9u; expect_zero(lantern_bitlist_resize(&state.justified_slots, 4), "initialize bitlist window"); state.justified_slots.bytes[0] = 0; - state.justified_slots.bytes[0] |= (uint8_t)(1u << 1u); /* slot offset + 1 */ - state.justified_slots_offset = 10u; + state.justified_slots.bytes[0] |= (uint8_t)(1u << 1u); /* slot anchor + 1 */ - assert(!lantern_state_slot_in_justified_window(&state, 9u)); + assert(lantern_state_slot_in_justified_window(&state, 9u)); assert(lantern_state_slot_in_justified_window(&state, 10u)); + assert(!lantern_state_slot_in_justified_window(&state, 14u)); bool bit = false; expect_zero(lantern_state_get_justified_slot_bit(&state, 11u, &bit), "read window bit"); assert(bit); - expect_zero(lantern_state_mark_justified_slot(&state, 9u), "mark trimmed slot"); - assert(state.justified_slots_offset == 10u); + expect_zero(lantern_state_mark_justified_slot(&state, 9u), "mark finalized slot"); + assert(state.justified_slots.bit_length == 4u); expect_zero(lantern_state_mark_justified_slot(&state, 14u), "mark new slot past window"); - assert(state.justified_slots_offset == 11u); + assert(state.justified_slots.bit_length == 5u); assert(lantern_state_slot_in_justified_window(&state, 14u)); - assert(!lantern_state_slot_in_justified_window(&state, 10u)); + assert(lantern_state_slot_in_justified_window(&state, 10u)); bool latest_bit = false; expect_zero(lantern_state_get_justified_slot_bit(&state, 14u, &latest_bit), "read latest bit"); @@ -2706,6 +3033,9 @@ int main(void) { if (test_state_transition_applies_block() != 0) { return 1; } + if (test_state_transition_rejects_missing_proposer_signature() != 0) { + return 1; + } if (test_state_transition_rejects_genesis_state_root_mismatch() != 0) { return 1; } diff --git a/tests/unit/test_storage.c b/tests/unit/test_storage.c index 764b6dd..8fa0c8e 100644 --- a/tests/unit/test_storage.c +++ b/tests/unit/test_storage.c @@ -16,6 +16,7 @@ #include "lantern/networking/messages.h" #include "lantern/storage/storage.h" #include "lantern/support/strings.h" +#include "../support/state_store_adapter.h" static void expect_zero(int rc, const char *label) { if (rc != 0) { @@ -191,8 +192,6 @@ int main(void) { LanternState state; lantern_state_init(&state); expect_zero(lantern_state_generate_genesis(&state, 123456u, 4u), "generate genesis"); - state.historical_roots_offset = 11u; - state.justified_slots_offset = 22u; /* Populate validator registry with deterministic pubkeys so SSZ encoding works */ const size_t genesis_validators = state.config.num_validators; @@ -217,8 +216,6 @@ int main(void) { return EXIT_FAILURE; } assert(loaded_state.config.num_validators == state.config.num_validators); - assert(loaded_state.historical_roots_offset == state.historical_roots_offset); - assert(loaded_state.justified_slots_offset == state.justified_slots_offset); lantern_state_reset(&loaded_state); LanternVote vote; @@ -228,11 +225,13 @@ int main(void) { signed_vote.data = vote; fill_signature(&signed_vote.signature, 0xAB); expect_zero(lantern_state_set_signed_validator_vote(&state, 1u, &signed_vote), "set validator vote"); - expect_zero(lantern_storage_save_votes(base_dir, &state), "save votes"); + expect_zero( + lantern_storage_save_votes(base_dir, &state, lantern_test_state_store_ensure(&state)), + "save votes"); lantern_state_clear_validator_vote(&state, 1u); expect_true(!lantern_state_validator_has_vote(&state, 1u), "vote cleared"); - int load_votes_rc = lantern_storage_load_votes(base_dir, &state); + int load_votes_rc = lantern_storage_load_votes(base_dir, &state, lantern_test_state_store_ensure(&state)); if (load_votes_rc != 0) { fprintf(stderr, "expected persisted votes rc=0 got %d\n", load_votes_rc); return EXIT_FAILURE; diff --git a/tools/lean-quickstart b/tools/lean-quickstart index d96968f..9b910e1 160000 --- a/tools/lean-quickstart +++ b/tools/lean-quickstart @@ -1 +1 @@ -Subproject commit d96968ff2a9a20a5e4307188c7aca5d1ef5559d2 +Subproject commit 9b910e1ae2ba478dbfd92ea81ae24c4c1eae32c6