From 27d46e41ac95ebefe7276acd3c48aee923107a16 Mon Sep 17 00:00:00 2001 From: uink45 <79078981+uink45@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:01:55 +1000 Subject: [PATCH] Prefer fork-choice finalized checkpoint; add test --- CMakeLists.txt | 15 +- src/core/client_http.c | 9 + src/core/client_reqresp.c | 9 + src/core/client_sync_blocks.c | 11 +- tests/unit/test_genesis_anchor.c | 368 ++++++++++++++++++++++++++++++- 5 files changed, 409 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c372c1..f096c57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -197,8 +197,21 @@ if(LANTERN_BUILD_TESTS) target_link_libraries(lantern_client_state_staleness_test PRIVATE lantern) add_test(NAME lantern_client_state_staleness COMMAND lantern_client_state_staleness_test) - add_executable(lantern_genesis_anchor_test tests/unit/test_genesis_anchor.c) + add_executable(lantern_genesis_anchor_test + tests/unit/test_genesis_anchor.c + tests/unit/client_test_helpers.c + ) target_link_libraries(lantern_genesis_anchor_test PRIVATE lantern) + target_include_directories( + lantern_genesis_anchor_test + PRIVATE + ${PROJECT_SOURCE_DIR}/tests/unit + ) + target_compile_definitions( + lantern_genesis_anchor_test + PRIVATE + LANTERN_TEST_FIXTURE_DIR="${PROJECT_SOURCE_DIR}/tests/fixtures" + ) add_test(NAME lantern_genesis_anchor COMMAND lantern_genesis_anchor_test) add_executable(lantern_genesis_bootstrap_test tests/unit/test_genesis_bootstrap.c) diff --git a/src/core/client_http.c b/src/core/client_http.c index 2a88bb0..d754237 100644 --- a/src/core/client_http.c +++ b/src/core/client_http.c @@ -615,6 +615,15 @@ int http_finalized_state_ssz_cb(void *context, uint8_t **out_bytes, size_t *out_ } LanternCheckpoint finalized = client->state.latest_finalized; + if (client->has_fork_choice) + { + const LanternCheckpoint *fork_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_finalized && !lantern_root_is_zero(&fork_finalized->root)) + { + finalized = *fork_finalized; + } + } lantern_client_unlock_state(client, state_locked); if (lantern_root_is_zero(&finalized.root)) diff --git a/src/core/client_reqresp.c b/src/core/client_reqresp.c index a1cd9de..ad61907 100644 --- a/src/core/client_reqresp.c +++ b/src/core/client_reqresp.c @@ -1083,6 +1083,15 @@ int reqresp_build_status(void *context, LanternStatusMessage *out_status) } out_status->finalized = client->state.latest_finalized; + if (client->has_fork_choice) + { + const LanternCheckpoint *fork_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_finalized && !lantern_root_is_zero(&fork_finalized->root)) + { + out_status->finalized = *fork_finalized; + } + } bool head_set = false; if (client->has_fork_choice) diff --git a/src/core/client_sync_blocks.c b/src/core/client_sync_blocks.c index 586942d..841b3ef 100644 --- a/src/core/client_sync_blocks.c +++ b/src/core/client_sync_blocks.c @@ -1544,7 +1544,16 @@ static bool rebuild_state_for_root_locked( LanternRoot replay_stop_root = {0}; char replay_stop_hex[ROOT_HEX_BUFFER_LEN] = {0}; - if (client->has_state) + if (client->has_fork_choice) + { + const LanternCheckpoint *fork_finalized = + lantern_fork_choice_latest_finalized(&client->fork_choice); + if (fork_finalized) + { + replay_stop_root = fork_finalized->root; + } + } + else if (client->has_state) { replay_stop_root = client->state.latest_finalized.root; } diff --git a/tests/unit/test_genesis_anchor.c b/tests/unit/test_genesis_anchor.c index a85d19f..c110d48 100644 --- a/tests/unit/test_genesis_anchor.c +++ b/tests/unit/test_genesis_anchor.c @@ -1,12 +1,22 @@ +#include #include -#include +#include +#include #include +#include +#include #include +#include #include "lantern/consensus/hash.h" #include "lantern/consensus/state.h" #include "lantern/core/client.h" +#include "lantern/storage/storage.h" +#include "lantern/support/strings.h" +#include "client_test_helpers.h" +#include "../../src/core/client_internal.h" +#include "../../src/core/client_services_internal.h" #include "../../src/core/client_sync_internal.h" static void fill_pubkeys(uint8_t *pubkeys, size_t count) @@ -42,6 +52,355 @@ static void fill_root(LanternRoot *root, uint8_t value) memset(root->bytes, value, LANTERN_ROOT_SIZE); } +static void cleanup_path(const char *path) +{ + if (!path) + { + return; + } + if (unlink(path) != 0 && errno != ENOENT) + { + fprintf(stderr, "failed to remove %s: %s\n", path, strerror(errno)); + } +} + +static void cleanup_dir(const char *path) +{ + if (!path) + { + return; + } + if (rmdir(path) != 0 && errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST) + { + fprintf(stderr, "failed to remove dir %s: %s\n", path, strerror(errno)); + } +} + +static void cleanup_storage_root_file( + const char *data_dir, + const char *subdir, + const LanternRoot *root, + const char *ext) +{ + if (!data_dir || !subdir || !root || !ext) + { + return; + } + + 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; + } + + char path[PATH_MAX]; + int written = snprintf(path, sizeof(path), "%s/%s/%s.%s", data_dir, subdir, root_hex, ext); + if (written <= 0 || (size_t)written >= sizeof(path)) + { + return; + } + cleanup_path(path); +} + +static int build_signed_head_block( + struct lantern_client *client, + struct PQSignatureSchemeSecretKey *secret, + LanternSignedBlock *out_block, + LanternRoot *out_root) +{ + if (!client || !secret || !out_block || !out_root) + { + return -1; + } + + int rc = -1; + LanternCheckpoint head = {0}; + LanternCheckpoint target = {0}; + LanternCheckpoint source = {0}; + LanternSignedVote proposer_vote; + memset(&proposer_vote, 0, sizeof(proposer_vote)); + + lantern_signed_block_with_attestation_init(out_block); + out_block->message.block.slot = client->state.slot + 1u; + if (lantern_proposer_for_slot( + out_block->message.block.slot, + client->state.config.num_validators, + &out_block->message.block.proposer_index) + != 0) + { + goto cleanup; + } + if (lantern_state_select_block_parent( + &client->state, + &client->store, + &out_block->message.block.parent_root) + != 0) + { + goto cleanup; + } + if (lantern_state_compute_vote_checkpoints( + &client->state, + &client->store, + &head, + &target, + &source) + != 0) + { + goto cleanup; + } + + proposer_vote.data.validator_id = out_block->message.block.proposer_index; + proposer_vote.data.slot = out_block->message.block.slot; + proposer_vote.data.head = head; + proposer_vote.data.target = target; + proposer_vote.data.source = source; + if (client_test_sign_vote_with_secret(&proposer_vote, secret) != 0) + { + goto cleanup; + } + + out_block->message.proposer_attestation = proposer_vote.data; + out_block->signatures.proposer_signature = proposer_vote.signature; + + if (lantern_state_preview_post_state_root( + &client->state, + &client->store, + out_block, + &out_block->message.block.state_root) + != 0) + { + goto cleanup; + } + if (lantern_hash_tree_root_block(&out_block->message.block, out_root) != 0) + { + goto cleanup; + } + + rc = 0; + +cleanup: + if (rc != 0) + { + lantern_signed_block_with_attestation_reset(out_block); + } + return rc; +} + +static int test_checkpoint_consumers_use_fork_choice_store(void) +{ + struct lantern_client client; + memset(&client, 0, sizeof(client)); + + struct PQSignatureSchemePublicKey *pub = NULL; + struct PQSignatureSchemeSecretKey *secret = NULL; + LanternRoot anchor_root = {0}; + LanternRoot child_root = {0}; + LanternRoot grandchild_root = {0}; + LanternSignedBlock grandchild_block; + bool grandchild_block_ready = false; + bool state_locked = false; + bool state_lock_held = false; + LanternState scratch; + lantern_state_init(&scratch); + + char dir_template[] = "/tmp/lantern_checkpoint_consumersXXXXXX"; + char *data_dir = NULL; + uint8_t *http_bytes = NULL; + size_t http_len = 0; + uint8_t *expected_bytes = NULL; + size_t expected_len = 0; + int rc = 1; + + if (client_test_setup_vote_validation_client( + &client, + "checkpoint_consumer_regression", + &pub, + &secret, + &anchor_root, + &child_root) + != 0) + { + fprintf(stderr, "failed to set up checkpoint consumer regression client\n"); + goto cleanup; + } + (void)anchor_root; + + if (build_signed_head_block(&client, secret, &grandchild_block, &grandchild_root) != 0) + { + fprintf(stderr, "failed to build grandchild block for checkpoint consumer regression\n"); + goto cleanup; + } + grandchild_block_ready = true; + + char *temp_dir = mkdtemp(dir_template); + if (!temp_dir) + { + fprintf(stderr, "failed to create temp data dir for checkpoint consumer regression\n"); + goto cleanup; + } + data_dir = strdup(temp_dir); + if (!data_dir) + { + fprintf(stderr, "failed to create temp data dir for checkpoint consumer regression\n"); + goto cleanup; + } + client.data_dir = data_dir; + + if (lantern_storage_store_state_for_root(data_dir, &child_root, &client.state) != 0) + { + fprintf(stderr, "failed to store child state snapshot for checkpoint consumer regression\n"); + goto cleanup; + } + if (lantern_storage_store_block_for_root(data_dir, &grandchild_root, &grandchild_block) != 0) + { + fprintf(stderr, "failed to store grandchild block for checkpoint consumer regression\n"); + goto cleanup; + } + + if (lantern_fork_choice_add_block( + &client.fork_choice, + &grandchild_block.message.block, + NULL, + &client.state.latest_justified, + &client.state.latest_finalized, + &grandchild_root) + != 0) + { + fprintf(stderr, "failed to add grandchild to fork choice for checkpoint consumer regression\n"); + goto cleanup; + } + + LanternCheckpoint child_checkpoint = { + .slot = client.state.slot, + .root = child_root, + }; + if (lantern_fork_choice_update_checkpoints( + &client.fork_choice, + &child_checkpoint, + &child_checkpoint) + != 0) + { + fprintf(stderr, "failed to move fork-choice checkpoints to child root\n"); + goto cleanup; + } + + LanternCheckpoint remote_finalized = client.state.latest_finalized; + fill_root(&remote_finalized.root, 0xA5u); + remote_finalized.slot = child_checkpoint.slot; + if (roots_equal(&remote_finalized.root, &child_root)) + { + remote_finalized.root.bytes[0] ^= 0x01u; + } + client.state.latest_finalized = remote_finalized; + + LanternStatusMessage status; + memset(&status, 0, sizeof(status)); + if (reqresp_build_status(&client, &status) != LANTERN_CLIENT_OK) + { + fprintf(stderr, "reqresp_build_status failed for checkpoint consumer regression\n"); + goto cleanup; + } + if (status.finalized.slot != child_checkpoint.slot + || !roots_equal(&status.finalized.root, &child_root)) + { + fprintf(stderr, "status finalized checkpoint did not follow fork choice store\n"); + goto cleanup; + } + if (roots_equal(&status.finalized.root, &client.state.latest_finalized.root)) + { + fprintf(stderr, "status finalized checkpoint incorrectly used client state root\n"); + goto cleanup; + } + + if (http_finalized_state_ssz_cb(&client, &http_bytes, &http_len) != LANTERN_HTTP_CB_OK) + { + fprintf(stderr, "http_finalized_state_ssz_cb failed for checkpoint consumer regression\n"); + goto cleanup; + } + if (lantern_storage_load_state_bytes_for_root( + data_dir, + &child_root, + &expected_bytes, + &expected_len) + != 0) + { + fprintf(stderr, "failed to load expected child state bytes for checkpoint consumer regression\n"); + goto cleanup; + } + if (http_len != expected_len || memcmp(http_bytes, expected_bytes, expected_len) != 0) + { + fprintf(stderr, "finalized HTTP endpoint did not load bytes for fork-choice finalized root\n"); + goto cleanup; + } + + state_locked = lantern_client_lock_state(&client); + if (client.state_lock_initialized && !state_locked) + { + fprintf(stderr, "failed to lock state for checkpoint consumer regression\n"); + goto cleanup; + } + state_lock_held = state_locked; + + bool used_scratch = false; + const LanternState *rebuilt = + lantern_client_state_for_root_locked(&client, &grandchild_root, &scratch, &used_scratch); + if (!rebuilt || !used_scratch) + { + fprintf(stderr, "grandchild state rebuild did not use finalized shortcut from fork choice\n"); + goto cleanup; + } + if (rebuilt->slot != grandchild_block.message.block.slot) + { + fprintf(stderr, "grandchild state rebuild returned wrong slot\n"); + goto cleanup; + } + if (!roots_equal(&client.state.latest_finalized.root, &remote_finalized.root)) + { + fprintf(stderr, "checkpoint consumer regression mutated client state\n"); + goto cleanup; + } + + rc = 0; + +cleanup: + if (state_lock_held) + { + lantern_client_unlock_state(&client, state_locked); + } + free(http_bytes); + free(expected_bytes); + lantern_state_reset(&scratch); + if (grandchild_block_ready) + { + lantern_signed_block_with_attestation_reset(&grandchild_block); + } + if (data_dir) + { + cleanup_storage_root_file(data_dir, "states", &child_root, "ssz"); + cleanup_storage_root_file(data_dir, "states", &child_root, "meta"); + cleanup_storage_root_file(data_dir, "states", &grandchild_root, "ssz"); + cleanup_storage_root_file(data_dir, "states", &grandchild_root, "meta"); + cleanup_storage_root_file(data_dir, "blocks", &grandchild_root, "ssz"); + char states_dir[PATH_MAX]; + char blocks_dir[PATH_MAX]; + int states_written = snprintf(states_dir, sizeof(states_dir), "%s/states", data_dir); + int blocks_written = snprintf(blocks_dir, sizeof(blocks_dir), "%s/blocks", data_dir); + if (states_written > 0 && (size_t)states_written < sizeof(states_dir)) + { + cleanup_dir(states_dir); + } + if (blocks_written > 0 && (size_t)blocks_written < sizeof(blocks_dir)) + { + cleanup_dir(blocks_dir); + } + client.data_dir = NULL; + cleanup_dir(data_dir); + free(data_dir); + } + client_test_teardown_vote_validation_client(&client, pub, secret); + return rc; +} + int main(void) { struct lantern_client client; @@ -244,6 +603,13 @@ int main(void) return 1; } + if (test_checkpoint_consumers_use_fork_choice_store() != 0) + { + lantern_state_reset(&client.state); + lantern_fork_choice_reset(&client.fork_choice); + return 1; + } + lantern_state_reset(&client.state); lantern_fork_choice_reset(&client.fork_choice); return 0;