From 881bb42fee91f978790b777bacc8b3c1027004f5 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:54:05 +1000 Subject: [PATCH] Improve syncing --- include/lantern/consensus/fork_choice.h | 14 +++ src/consensus/fork_choice.c | 50 +++++++++ src/core/client_reqresp_blocks.c | 2 +- src/core/client_sync.c | 28 ++++- src/core/client_sync_blocks.c | 21 +++- src/core/client_sync_internal.h | 2 - tests/unit/test_fork_choice.c | 82 +++++++++++++++ tests/unit/test_genesis_anchor.c | 131 ++++++++++++++++++++++++ tests/unit/test_state.c | 69 +++++++++++++ 9 files changed, 388 insertions(+), 11 deletions(-) diff --git a/include/lantern/consensus/fork_choice.h b/include/lantern/consensus/fork_choice.h index a48ec5d..80c6974 100644 --- a/include/lantern/consensus/fork_choice.h +++ b/include/lantern/consensus/fork_choice.h @@ -94,6 +94,20 @@ int lantern_fork_choice_update_checkpoints( const LanternCheckpoint *latest_justified, const LanternCheckpoint *latest_finalized); +/** + * Restore fork-choice checkpoints from persisted state. + * + * Unlike lantern_fork_choice_update_checkpoints(), this API is intended for + * startup restoration and may move checkpoints backwards when the persisted + * state is behind the temporary anchor checkpoints used during init. + * + * Any provided checkpoint root must already exist in the fork-choice store. + */ +int lantern_fork_choice_restore_checkpoints( + LanternForkChoice *store, + const LanternCheckpoint *latest_justified, + const LanternCheckpoint *latest_finalized); + int lantern_fork_choice_accept_new_votes(LanternForkChoice *store); int lantern_fork_choice_update_safe_target(LanternForkChoice *store); int lantern_fork_choice_recompute_head(LanternForkChoice *store); diff --git a/src/consensus/fork_choice.c b/src/consensus/fork_choice.c index a22ab5f..255d0c4 100644 --- a/src/consensus/fork_choice.c +++ b/src/consensus/fork_choice.c @@ -842,6 +842,56 @@ int lantern_fork_choice_update_checkpoints( return update_global_checkpoints(store, latest_justified, latest_finalized); } +int lantern_fork_choice_restore_checkpoints( + LanternForkChoice *store, + const LanternCheckpoint *latest_justified, + const LanternCheckpoint *latest_finalized) { + if (!store || !store->initialized || !store->has_anchor) { + return -1; + } + + LanternCheckpoint restored_justified = store->latest_justified; + LanternCheckpoint restored_finalized = store->latest_finalized; + bool justified_changed = false; + + if (latest_justified && !root_is_zero(&latest_justified->root)) { + size_t justified_index = 0; + if (!map_lookup(store, &latest_justified->root, &justified_index)) { + return -1; + } + restored_justified = *latest_justified; + justified_changed = true; + } + if (latest_finalized && !root_is_zero(&latest_finalized->root)) { + size_t finalized_index = 0; + if (!map_lookup(store, &latest_finalized->root, &finalized_index)) { + return -1; + } + restored_finalized = *latest_finalized; + } + if (restored_finalized.slot > restored_justified.slot) { + return -1; + } + + LanternCheckpoint previous_justified = store->latest_justified; + LanternCheckpoint previous_finalized = store->latest_finalized; + LanternRoot previous_head = store->head; + bool previous_has_head = store->has_head; + + store->latest_justified = restored_justified; + if (justified_changed && lantern_fork_choice_recompute_head(store) != 0) { + store->latest_justified = previous_justified; + store->latest_finalized = previous_finalized; + store->head = previous_head; + store->has_head = previous_has_head; + return -1; + } + + store->latest_justified = restored_justified; + store->latest_finalized = restored_finalized; + return 0; +} + static int find_start_index( const LanternForkChoice *store, const LanternRoot *start_root, diff --git a/src/core/client_reqresp_blocks.c b/src/core/client_reqresp_blocks.c index b661853..466bb8f 100644 --- a/src/core/client_reqresp_blocks.c +++ b/src/core/client_reqresp_blocks.c @@ -776,7 +776,7 @@ static int schedule_blocks_request_batch( { return LANTERN_CLIENT_ERR_INVALID_PARAM; } - if (root_count > LANTERN_MAX_BLOCKS_PER_REQUEST) + if (root_count > LANTERN_MAX_REQUEST_BLOCKS) { return LANTERN_CLIENT_ERR_INVALID_PARAM; } diff --git a/src/core/client_sync.c b/src/core/client_sync.c index 83a3788..1e53b2c 100644 --- a/src/core/client_sync.c +++ b/src/core/client_sync.c @@ -562,6 +562,14 @@ int initialize_fork_choice(struct lantern_client *client) anchor_checkpoint.root = anchor_root; anchor_checkpoint.slot = anchor.slot; + /* + * Seed fork-choice with anchor_checkpoint (whose root matches the anchor + * block that set_anchor registers in the tree). This guarantees + * lmd_ghost_compute can always find its start_root during block restore. + * + * Real persisted checkpoints are synced to the store AFTER + * restore_persisted_blocks() via lantern_fork_choice_restore_checkpoints(). + */ if (lantern_fork_choice_set_anchor( &client->fork_choice, &anchor, @@ -651,7 +659,6 @@ 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); } - /** * Restore persisted blocks from storage into fork choice. * @@ -734,6 +741,17 @@ int restore_persisted_blocks(struct lantern_client *client) &(const struct lantern_log_metadata){.validator = client->node_id}, "advancing fork choice time after restore failed"); } + if (lantern_fork_choice_restore_checkpoints( + &client->fork_choice, + &client->state.latest_justified, + &client->state.latest_finalized) + != 0) + { + lantern_log_warn( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "restoring persisted checkpoints after block restore failed"); + } persisted_block_list_reset(&list); return LANTERN_CLIENT_OK; @@ -1238,7 +1256,7 @@ static bool try_schedule_blocks_request_batch( { return false; } - if (root_count > LANTERN_MAX_BLOCKS_PER_REQUEST) + if (root_count > LANTERN_MAX_REQUEST_BLOCKS) { return false; } @@ -1898,8 +1916,8 @@ void lantern_client_request_pending_parent_after_blocks( pending_parent_candidate_compare); } - LanternRoot request_roots[LANTERN_MAX_BLOCKS_PER_REQUEST]; - uint32_t request_depths[LANTERN_MAX_BLOCKS_PER_REQUEST]; + LanternRoot request_roots[LANTERN_MAX_REQUEST_BLOCKS]; + uint32_t request_depths[LANTERN_MAX_REQUEST_BLOCKS]; size_t request_count = 0; if (prefer_requested_root) @@ -1916,7 +1934,7 @@ void lantern_client_request_pending_parent_after_blocks( for (size_t i = 0; i < candidate_count; ++i) { - if (request_count >= LANTERN_MAX_BLOCKS_PER_REQUEST) + if (request_count >= LANTERN_MAX_REQUEST_BLOCKS) { break; } diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index acf0c75..5bfece3 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -987,6 +987,21 @@ 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( + &client->fork_choice, + &client->state.latest_justified, + &client->state.latest_finalized) + != 0) + { + lantern_log_warn( + "forkchoice", + &(const struct lantern_log_metadata){.validator = client->node_id}, + "failed to sync fork choice checkpoints when adopting state slot=%" PRIu64, + client->state.slot); + } + } lantern_state_reset(&previous); } @@ -1819,7 +1834,7 @@ bool lantern_client_import_block( bool have_replay_state = false; bool processed = false; bool deferred = false; - LanternRoot missing_roots[LANTERN_MAX_BLOCKS_PER_REQUEST]; + LanternRoot missing_roots[LANTERN_MAX_REQUEST_BLOCKS]; size_t missing_count = 0; if (rebuild_state_for_root_locked( @@ -1827,7 +1842,7 @@ bool lantern_client_import_block( &parent_root, &replay_state, missing_roots, - LANTERN_MAX_BLOCKS_PER_REQUEST, + LANTERN_MAX_REQUEST_BLOCKS, &missing_count)) { have_replay_state = true; @@ -1869,7 +1884,7 @@ bool lantern_client_import_block( true); if (missing_count > 0) { - uint32_t request_depths[LANTERN_MAX_BLOCKS_PER_REQUEST]; + uint32_t request_depths[LANTERN_MAX_REQUEST_BLOCKS]; uint32_t request_depth = backfill_depth + 1u; if (request_depth > LANTERN_MAX_BACKFILL_DEPTH) { diff --git a/src/core/client_sync_internal.h b/src/core/client_sync_internal.h index ccced25..02d2b14 100644 --- a/src/core/client_sync_internal.h +++ b/src/core/client_sync_internal.h @@ -46,8 +46,6 @@ typedef struct peer_id peer_id_t; * Constants * ============================================================================ */ -/** Maximum roots per blocks_by_root request */ -#define LANTERN_MAX_BLOCKS_PER_REQUEST 10u /** * Maximum parent depth for ancestor backfill requests. * diff --git a/tests/unit/test_fork_choice.c b/tests/unit/test_fork_choice.c index 533f8b1..a23d80d 100644 --- a/tests/unit/test_fork_choice.c +++ b/tests/unit/test_fork_choice.c @@ -432,6 +432,85 @@ static int test_fork_choice_checkpoint_progression(void) { return 0; } +static int test_fork_choice_restore_checkpoints(void) { + LanternForkChoice store; + lantern_fork_choice_init(&store); + + LanternConfig config = {.num_validators = 4, .genesis_time = 1}; + 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); + LanternCheckpoint block_one_cp = make_checkpoint(&block_one_root, block_one.slot); + assert( + lantern_fork_choice_add_block( + &store, + &block_one, + NULL, + NULL, + NULL, + &block_one_root) + == 0); + + LanternBlock block_two; + init_block(&block_two, 2, 1, &block_one_root, 0x43); + LanternRoot block_two_root; + assert(lantern_hash_tree_root_block(&block_two, &block_two_root) == 0); + LanternCheckpoint block_two_cp = make_checkpoint(&block_two_root, block_two.slot); + assert( + lantern_fork_choice_add_block( + &store, + &block_two, + NULL, + NULL, + NULL, + &block_two_root) + == 0); + + assert(lantern_fork_choice_update_checkpoints(&store, &block_two_cp, &block_one_cp) == 0); + const LanternCheckpoint *latest_justified = lantern_fork_choice_latest_justified(&store); + const LanternCheckpoint *latest_finalized = lantern_fork_choice_latest_finalized(&store); + assert(latest_justified && checkpoints_equal(latest_justified, &block_two_cp)); + assert(latest_finalized && checkpoints_equal(latest_finalized, &block_one_cp)); + + assert(lantern_fork_choice_restore_checkpoints(&store, &block_one_cp, &genesis_cp) == 0); + latest_justified = lantern_fork_choice_latest_justified(&store); + latest_finalized = lantern_fork_choice_latest_finalized(&store); + assert(latest_justified && checkpoints_equal(latest_justified, &block_one_cp)); + assert(latest_finalized && checkpoints_equal(latest_finalized, &genesis_cp)); + + LanternRoot head_before_failure; + assert(lantern_fork_choice_current_head(&store, &head_before_failure) == 0); + + LanternCheckpoint unknown_cp = block_one_cp; + fill_root(&unknown_cp.root, 0xEE); + assert(lantern_fork_choice_restore_checkpoints(&store, &unknown_cp, &genesis_cp) != 0); + + const LanternCheckpoint *justified_after_failure = lantern_fork_choice_latest_justified(&store); + const LanternCheckpoint *finalized_after_failure = lantern_fork_choice_latest_finalized(&store); + assert(justified_after_failure && checkpoints_equal(justified_after_failure, &block_one_cp)); + assert(finalized_after_failure && checkpoints_equal(finalized_after_failure, &genesis_cp)); + + LanternRoot head_after_failure; + assert(lantern_fork_choice_current_head(&store, &head_after_failure) == 0); + assert(roots_equal(&head_after_failure, &head_before_failure)); + + lantern_fork_choice_reset(&store); + reset_block(&block_two); + reset_block(&block_one); + reset_block(&genesis); + return 0; +} + static int test_fork_choice_advance_time_schedules_votes(void) { LanternForkChoice store; lantern_fork_choice_init(&store); @@ -581,6 +660,9 @@ int main(void) { if (test_fork_choice_checkpoint_progression() != 0) { return 1; } + if (test_fork_choice_restore_checkpoints() != 0) { + return 1; + } if (test_fork_choice_advance_time_schedules_votes() != 0) { return 1; } diff --git a/tests/unit/test_genesis_anchor.c b/tests/unit/test_genesis_anchor.c index 4c05ec1..a85d19f 100644 --- a/tests/unit/test_genesis_anchor.c +++ b/tests/unit/test_genesis_anchor.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -32,6 +33,15 @@ static int roots_equal(const LanternRoot *left, const LanternRoot *right) return memcmp(left->bytes, right->bytes, LANTERN_ROOT_SIZE) == 0; } +static void fill_root(LanternRoot *root, uint8_t value) +{ + if (!root) + { + return; + } + memset(root->bytes, value, LANTERN_ROOT_SIZE); +} + int main(void) { struct lantern_client client; @@ -113,6 +123,127 @@ int main(void) return 1; } + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + + /* + * Restart regression: initialize_fork_choice must preserve persisted + * justified/finalized checkpoints for non-genesis snapshots. + */ + memset(&client, 0, sizeof(client)); + client.node_id = "fork_choice_checkpoint_restore"; + client.has_state = true; + lantern_state_init(&client.state); + + if (lantern_state_generate_genesis(&client.state, UINT64_C(1761717362), 4u) != 0) + { + fprintf(stderr, "failed to generate restart regression state\n"); + return 1; + } + + uint8_t restart_pubkeys[4u * LANTERN_VALIDATOR_PUBKEY_SIZE]; + fill_pubkeys(restart_pubkeys, 4u); + if (lantern_state_set_validator_pubkeys(&client.state, restart_pubkeys, 4u) != 0) + { + fprintf(stderr, "failed to set validator pubkeys for restart regression\n"); + lantern_state_reset(&client.state); + return 1; + } + + client.state.slot = 447u; + client.state.latest_block_header.slot = 443u; + client.state.latest_block_header.proposer_index = 1u; + fill_root(&client.state.latest_block_header.parent_root, 0x55u); + + LanternCheckpoint expected_justified; + LanternCheckpoint expected_finalized; + fill_root(&expected_justified.root, 0x39u); + expected_justified.slot = 439u; + fill_root(&expected_finalized.root, 0x34u); + expected_finalized.slot = 434u; + client.state.latest_justified = expected_justified; + client.state.latest_finalized = expected_finalized; + + LanternRoot restart_state_root; + if (lantern_hash_tree_root_state(&client.state, &restart_state_root) != 0) + { + fprintf(stderr, "failed to hash restart regression state\n"); + lantern_state_reset(&client.state); + return 1; + } + LanternBlockHeader restart_anchor_header = client.state.latest_block_header; + restart_anchor_header.state_root = restart_state_root; + LanternRoot expected_restart_anchor_root; + if (lantern_hash_tree_root_block_header(&restart_anchor_header, &expected_restart_anchor_root) != 0) + { + fprintf(stderr, "failed to hash restart regression anchor header\n"); + lantern_state_reset(&client.state); + return 1; + } + + if (initialize_fork_choice(&client) != LANTERN_CLIENT_OK) + { + fprintf(stderr, "initialize_fork_choice failed for restart regression\n"); + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + + LanternRoot restart_head; + if (lantern_fork_choice_current_head(&client.fork_choice, &restart_head) != 0) + { + fprintf(stderr, "failed to read restart regression fork choice head\n"); + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + if (!roots_equal(&restart_head, &expected_restart_anchor_root)) + { + fprintf(stderr, "restart regression head mismatch\n"); + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + + /* + * After initialize_fork_choice the store checkpoints match the anchor + * (not the persisted state checkpoints). Real persisted checkpoints are + * synced later by restore_persisted_blocks → restore_checkpoints. + */ + const LanternCheckpoint *store_justified = + lantern_fork_choice_latest_justified(&client.fork_choice); + const LanternCheckpoint *store_finalized = + lantern_fork_choice_latest_finalized(&client.fork_choice); + if (!store_justified || !store_finalized) + { + fprintf(stderr, "missing fork-choice checkpoints after restart init\n"); + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + if (store_justified->slot != restart_anchor_header.slot + || !roots_equal(&store_justified->root, &expected_restart_anchor_root)) + { + fprintf(stderr, + "justified checkpoint should match anchor after init " + "(got slot=%" PRIu64 " expected slot=%" PRIu64 ")\n", + store_justified->slot, (uint64_t)restart_anchor_header.slot); + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + if (store_finalized->slot != restart_anchor_header.slot + || !roots_equal(&store_finalized->root, &expected_restart_anchor_root)) + { + fprintf(stderr, + "finalized checkpoint should match anchor after init " + "(got slot=%" PRIu64 " expected slot=%" PRIu64 ")\n", + store_finalized->slot, (uint64_t)restart_anchor_header.slot); + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + lantern_state_reset(&client.state); lantern_fork_choice_reset(&client.fork_choice); return 0; diff --git a/tests/unit/test_state.c b/tests/unit/test_state.c index 241287d..22602f8 100644 --- a/tests/unit/test_state.c +++ b/tests/unit/test_state.c @@ -2008,6 +2008,72 @@ static int test_compute_vote_checkpoints_respects_safe_target(void) { return 0; } +static int test_compute_vote_checkpoints_uses_store_source_when_store_ahead(void) { + LanternState state; + LanternForkChoice fork_choice; + LanternRoot genesis_root; + setup_state_and_fork_choice(&state, &fork_choice, 1650, 8, &genesis_root); + + LanternRoot block_roots[5]; + block_roots[0] = genesis_root; + LanternRoot parent_root = genesis_root; + for (uint64_t slot = 1; slot <= 4; ++slot) { + LanternBlock block; + LanternRoot block_root; + make_block(&state, slot, &parent_root, &block, &block_root); + expect_zero( + lantern_fork_choice_add_block(&fork_choice, &block, NULL, NULL, NULL, &block_root), + "add block for source precedence test"); + block_roots[slot] = block_root; + parent_root = block_root; + lantern_block_body_reset(&block.body); + } + + fork_choice.head = block_roots[4]; + fork_choice.has_head = true; + fork_choice.safe_target = block_roots[4]; + fork_choice.has_safe_target = true; + + state.latest_finalized.slot = 0; + state.latest_finalized.root = block_roots[0]; + state.latest_justified.slot = 1; + state.latest_justified.root = block_roots[1]; + + LanternCheckpoint store_justified; + store_justified.slot = 3; + store_justified.root = block_roots[3]; + expect_zero( + lantern_fork_choice_update_checkpoints(&fork_choice, &store_justified, &state.latest_finalized), + "advance fork-choice justified beyond state justified"); + + LanternCheckpoint head; + LanternCheckpoint target; + LanternCheckpoint source; + int rc = lantern_state_compute_vote_checkpoints(&state, &head, &target, &source); + if (rc != 0) { + fprintf(stderr, "compute vote checkpoints store source precedence failed (rc=%d)\n", rc); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + if (!checkpoints_equal(&source, &store_justified)) { + fprintf(stderr, "source checkpoint should follow fork-choice latest_justified when store is ahead\n"); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + if (checkpoints_equal(&source, &state.latest_justified)) { + fprintf(stderr, "source checkpoint incorrectly used state latest_justified while store is ahead\n"); + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 1; + } + + lantern_state_reset(&state); + lantern_fork_choice_reset(&fork_choice); + return 0; +} + static int test_compute_vote_checkpoints_justifiable(void) { LanternState state; LanternForkChoice fork_choice; @@ -2385,6 +2451,9 @@ int main(void) { if (test_compute_vote_checkpoints_basic() != 0) { return 1; } + if (test_compute_vote_checkpoints_uses_store_source_when_store_ahead() != 0) { + return 1; + } if (test_compute_vote_checkpoints_respects_safe_target() != 0) { return 1; }