diff --git a/CLAUDE.md b/CLAUDE.md index f94562e..19864d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -372,3 +372,6 @@ cargo test -p ethlambda-blockchain --features skip-signature-verification --test - zeam (Zig): - ream (Rust): - qlean (C++): +- grandine (Rust): +- gean (Go): +- Lantern (C): diff --git a/Cargo.lock b/Cargo.lock index 7558bbf..ff2283d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1945,11 +1945,14 @@ dependencies = [ "ethlambda-rpc", "ethlambda-storage", "ethlambda-types", + "eyre", "hex", + "reqwest", "serde", "serde_yaml_ng", "spawned-concurrency", "spawned-rt", + "thiserror 2.0.17", "tokio", "tracing", "tracing-subscriber", @@ -2354,6 +2357,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fancy-regex" version = "0.14.0" @@ -2956,12 +2969,30 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.5", +] + [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64", "bytes", "futures-channel", "futures-core", @@ -2969,7 +3000,9 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2 0.6.1", "tokio", @@ -3186,6 +3219,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "2.12.1" @@ -3225,6 +3264,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -5972,6 +6021,44 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.5", +] + [[package]] name = "resolv-conf" version = "0.7.6" @@ -6811,6 +6898,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -7046,6 +7136,16 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -7118,6 +7218,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" diff --git a/Cargo.toml b/Cargo.toml index 07efe5e..33ed1ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,5 @@ vergen-git2 = { version = "9", features = ["rustc"] } rand = "0.9" rocksdb = "0.24" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } +eyre = "0.6" diff --git a/bin/ethlambda/Cargo.toml b/bin/ethlambda/Cargo.toml index 7300f93..7a89768 100644 --- a/bin/ethlambda/Cargo.toml +++ b/bin/ethlambda/Cargo.toml @@ -24,6 +24,9 @@ serde_yaml_ng.workspace = true hex.workspace = true clap.workspace = true +reqwest.workspace = true +thiserror.workspace = true +eyre.workspace = true [build-dependencies] vergen-git2.workspace = true diff --git a/bin/ethlambda/src/checkpoint_sync.rs b/bin/ethlambda/src/checkpoint_sync.rs new file mode 100644 index 0000000..8b163ad --- /dev/null +++ b/bin/ethlambda/src/checkpoint_sync.rs @@ -0,0 +1,417 @@ +use std::time::Duration; + +use ethlambda_types::primitives::ssz::{Decode, DecodeError, TreeHash}; +use ethlambda_types::state::{State, Validator}; +use reqwest::Client; + +/// Timeout for establishing the HTTP connection to the checkpoint peer. +/// Fail fast if the peer is unreachable. +const CHECKPOINT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15); + +/// Timeout for reading data during body download. +/// This is an inactivity timeout - it resets on each successful read. +const CHECKPOINT_READ_TIMEOUT: Duration = Duration::from_secs(15); + +#[derive(Debug, thiserror::Error)] +pub enum CheckpointSyncError { + #[error("HTTP request failed: {0}")] + Http(#[from] reqwest::Error), + #[error("SSZ deserialization failed: {0:?}")] + SszDecode(DecodeError), + #[error("checkpoint state slot cannot be 0")] + SlotIsZero, + #[error("checkpoint state has no validators")] + NoValidators, + #[error("genesis time mismatch: expected {expected}, got {got}")] + GenesisTimeMismatch { expected: u64, got: u64 }, + #[error("validator count mismatch: expected {expected}, got {got}")] + ValidatorCountMismatch { expected: usize, got: usize }, + #[error( + "validator at position {position} has non-sequential index (expected {expected}, got {got})" + )] + NonSequentialValidatorIndex { + position: usize, + expected: u64, + got: u64, + }, + #[error("validator {index} pubkey mismatch")] + ValidatorPubkeyMismatch { index: usize }, + #[error("finalized slot cannot exceed state slot")] + FinalizedExceedsStateSlot, + #[error("justified slot cannot precede finalized slot")] + JustifiedPrecedesFinalized, + #[error("justified and finalized at same slot must have matching roots")] + JustifiedFinalizedRootMismatch, + #[error("block header slot exceeds state slot")] + BlockHeaderSlotExceedsState, + #[error("block header at finalized slot must match finalized root")] + BlockHeaderFinalizedRootMismatch, + #[error("block header at justified slot must match justified root")] + BlockHeaderJustifiedRootMismatch, +} + +/// Fetch finalized state from checkpoint sync URL. +/// +/// Uses two-phase timeout strategy: +/// - Connect timeout (15s): Fails quickly if peer is unreachable +/// - Read timeout (15s): Inactivity timeout that resets on each read +/// +/// Note: We use a read timeout (via `.read_timeout()`) instead of a total download +/// timeout to automatically detect stalled downloads. This allows large states +/// to be downloaded successfully as long as data keeps flowing, while still +/// failing fast if the connection stalls. A plain total timeout would +/// disconnect even for valid downloads if the state is simply too large to +/// transfer within the time limit. +pub async fn fetch_checkpoint_state( + base_url: &str, + expected_genesis_time: u64, + expected_validators: &[Validator], +) -> Result { + let base_url = base_url.trim_end_matches('/'); + let url = format!("{base_url}/lean/v0/states/finalized"); + // Use .read_timeout() to detect stalled downloads (inactivity timer). + // This allows large states to complete as long as data keeps flowing. + let client = Client::builder() + .connect_timeout(CHECKPOINT_CONNECT_TIMEOUT) + .read_timeout(CHECKPOINT_READ_TIMEOUT) + .build()?; + + let response = client + .get(&url) + .header("Accept", "application/octet-stream") + .send() + .await? + .error_for_status()?; + + let bytes = response.bytes().await?; + let state = State::from_ssz_bytes(&bytes).map_err(CheckpointSyncError::SszDecode)?; + + verify_checkpoint_state(&state, expected_genesis_time, expected_validators)?; + + Ok(state) +} + +/// Verify checkpoint state is structurally valid. +/// +/// Arguments: +/// - state: The downloaded checkpoint state +/// - expected_genesis_time: Genesis time from local config +/// - expected_validators: Validator pubkeys from local genesis config +fn verify_checkpoint_state( + state: &State, + expected_genesis_time: u64, + expected_validators: &[Validator], +) -> Result<(), CheckpointSyncError> { + // Slot sanity check + if state.slot == 0 { + return Err(CheckpointSyncError::SlotIsZero); + } + + // Validators exist + if state.validators.is_empty() { + return Err(CheckpointSyncError::NoValidators); + } + + // Genesis time matches + if state.config.genesis_time != expected_genesis_time { + return Err(CheckpointSyncError::GenesisTimeMismatch { + expected: expected_genesis_time, + got: state.config.genesis_time, + }); + } + + // Validator count matches + if state.validators.len() != expected_validators.len() { + return Err(CheckpointSyncError::ValidatorCountMismatch { + expected: expected_validators.len(), + got: state.validators.len(), + }); + } + + // Validator indices are sequential (0, 1, 2, ...) + for (position, validator) in state.validators.iter().enumerate() { + if validator.index != position as u64 { + return Err(CheckpointSyncError::NonSequentialValidatorIndex { + position, + expected: position as u64, + got: validator.index, + }); + } + } + + // Validator pubkeys match (critical security check) + for (i, (state_val, expected_val)) in state + .validators + .iter() + .zip(expected_validators.iter()) + .enumerate() + { + if state_val.pubkey != expected_val.pubkey { + return Err(CheckpointSyncError::ValidatorPubkeyMismatch { index: i }); + } + } + + // Finalized slot sanity + if state.latest_finalized.slot > state.slot { + return Err(CheckpointSyncError::FinalizedExceedsStateSlot); + } + + // Justified must be at or after finalized + if state.latest_justified.slot < state.latest_finalized.slot { + return Err(CheckpointSyncError::JustifiedPrecedesFinalized); + } + + // If justified and finalized are at same slot, roots must match + if state.latest_justified.slot == state.latest_finalized.slot + && state.latest_justified.root != state.latest_finalized.root + { + return Err(CheckpointSyncError::JustifiedFinalizedRootMismatch); + } + + // Block header slot consistency + if state.latest_block_header.slot > state.slot { + return Err(CheckpointSyncError::BlockHeaderSlotExceedsState); + } + + // If block header matches checkpoint slots, roots must match + let block_root = state.latest_block_header.tree_hash_root(); + + if state.latest_block_header.slot == state.latest_finalized.slot + && block_root != state.latest_finalized.root.0 + { + return Err(CheckpointSyncError::BlockHeaderFinalizedRootMismatch); + } + + if state.latest_block_header.slot == state.latest_justified.slot + && block_root != state.latest_justified.root.0 + { + return Err(CheckpointSyncError::BlockHeaderJustifiedRootMismatch); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use ethlambda_types::block::BlockHeader; + use ethlambda_types::primitives::VariableList; + use ethlambda_types::state::{ChainConfig, Checkpoint}; + + // Helper to create valid test state + fn create_test_state(slot: u64, validators: Vec, genesis_time: u64) -> State { + use ethlambda_types::primitives::H256; + use ethlambda_types::state::{JustificationValidators, JustifiedSlots}; + + State { + slot, + validators: VariableList::new(validators).unwrap(), + latest_block_header: BlockHeader { + slot, + parent_root: H256::ZERO, + state_root: H256::ZERO, + body_root: H256::ZERO, + proposer_index: 0, + }, + latest_justified: Checkpoint { + slot: slot.saturating_sub(10), + root: H256::ZERO, + }, + latest_finalized: Checkpoint { + slot: slot.saturating_sub(20), + root: H256::ZERO, + }, + config: ChainConfig { genesis_time }, + historical_block_hashes: Default::default(), + justified_slots: JustifiedSlots::with_capacity(0).unwrap(), + justifications_roots: Default::default(), + justifications_validators: JustificationValidators::with_capacity(0).unwrap(), + } + } + + fn create_test_validator() -> Validator { + Validator { + pubkey: [1u8; 52], + index: 0, + } + } + + fn create_different_validator() -> Validator { + Validator { + pubkey: [2u8; 52], + index: 0, + } + } + + fn create_validators_with_indices(count: usize) -> Vec { + (0..count) + .map(|i| Validator { + pubkey: [i as u8 + 1; 52], + index: i as u64, + }) + .collect() + } + + #[test] + fn verify_accepts_valid_state() { + let validators = vec![create_test_validator()]; + let state = create_test_state(100, validators.clone(), 1000); + assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok()); + } + + #[test] + fn verify_rejects_slot_zero() { + let validators = vec![create_test_validator()]; + let state = create_test_state(0, validators.clone(), 1000); + assert!(verify_checkpoint_state(&state, 1000, &validators).is_err()); + } + + #[test] + fn verify_rejects_empty_validators() { + let state = create_test_state(100, vec![], 1000); + assert!(verify_checkpoint_state(&state, 1000, &[]).is_err()); + } + + #[test] + fn verify_rejects_genesis_time_mismatch() { + let validators = vec![create_test_validator()]; + let state = create_test_state(100, validators.clone(), 1000); + // State has genesis_time=1000, we pass expected=9999 + assert!(verify_checkpoint_state(&state, 9999, &validators).is_err()); + } + + #[test] + fn verify_rejects_validator_count_mismatch() { + let validators = vec![create_test_validator()]; + let state = create_test_state(100, validators.clone(), 1000); + let extra_validators = create_validators_with_indices(2); + assert!(verify_checkpoint_state(&state, 1000, &extra_validators).is_err()); + } + + #[test] + fn verify_accepts_multiple_validators_with_sequential_indices() { + let validators = create_validators_with_indices(3); + let state = create_test_state(100, validators.clone(), 1000); + assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok()); + } + + #[test] + fn verify_rejects_non_sequential_validator_indices() { + let mut validators = create_validators_with_indices(3); + validators[1].index = 5; // Wrong index at position 1 + let state = create_test_state(100, validators.clone(), 1000); + let expected_validators = create_validators_with_indices(3); + assert!(verify_checkpoint_state(&state, 1000, &expected_validators).is_err()); + } + + #[test] + fn verify_rejects_duplicate_validator_indices() { + let mut validators = create_validators_with_indices(3); + validators[2].index = 0; // Duplicate index + let state = create_test_state(100, validators.clone(), 1000); + let expected_validators = create_validators_with_indices(3); + assert!(verify_checkpoint_state(&state, 1000, &expected_validators).is_err()); + } + + #[test] + fn verify_rejects_validator_pubkey_mismatch() { + let validators = vec![create_test_validator()]; + let state = create_test_state(100, validators.clone(), 1000); + let different_validators = vec![create_different_validator()]; + assert!(verify_checkpoint_state(&state, 1000, &different_validators).is_err()); + } + + #[test] + fn verify_rejects_finalized_after_state_slot() { + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_finalized.slot = 101; // Finalized after state slot + assert!(verify_checkpoint_state(&state, 1000, &validators).is_err()); + } + + #[test] + fn verify_rejects_justified_before_finalized() { + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_finalized.slot = 50; + state.latest_justified.slot = 40; // Justified before finalized + assert!(verify_checkpoint_state(&state, 1000, &validators).is_err()); + } + + #[test] + fn verify_accepts_justified_equals_finalized_with_matching_roots() { + use ethlambda_types::primitives::H256; + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + let common_root = H256::from([42u8; 32]); + state.latest_finalized.slot = 50; + state.latest_finalized.root = common_root; + state.latest_justified.slot = 50; // Same slot + state.latest_justified.root = common_root; // Same root + assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok()); + } + + #[test] + fn verify_rejects_justified_equals_finalized_with_different_roots() { + use ethlambda_types::primitives::H256; + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_finalized.slot = 50; + state.latest_finalized.root = H256::from([1u8; 32]); + state.latest_justified.slot = 50; // Same slot + state.latest_justified.root = H256::from([2u8; 32]); // Different root - conflict! + assert!(verify_checkpoint_state(&state, 1000, &validators).is_err()); + } + + #[test] + fn verify_rejects_block_header_slot_exceeds_state() { + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_block_header.slot = 101; // Block header slot exceeds state slot + assert!(verify_checkpoint_state(&state, 1000, &validators).is_err()); + } + + #[test] + fn verify_accepts_block_header_matches_finalized_with_correct_root() { + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_block_header.slot = 50; + let block_root = state.latest_block_header.tree_hash_root(); + state.latest_finalized.slot = 50; + state.latest_finalized.root = block_root; + assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok()); + } + + #[test] + fn verify_rejects_block_header_matches_finalized_with_wrong_root() { + use ethlambda_types::primitives::H256; + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_block_header.slot = 50; + state.latest_finalized.slot = 50; + state.latest_finalized.root = H256::from([99u8; 32]); // Wrong root + assert!(verify_checkpoint_state(&state, 1000, &validators).is_err()); + } + + #[test] + fn verify_accepts_block_header_matches_justified_with_correct_root() { + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_block_header.slot = 90; + let block_root = state.latest_block_header.tree_hash_root(); + state.latest_justified.slot = 90; + state.latest_justified.root = block_root; + assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok()); + } + + #[test] + fn verify_rejects_block_header_matches_justified_with_wrong_root() { + use ethlambda_types::primitives::H256; + let validators = vec![create_test_validator()]; + let mut state = create_test_state(100, validators.clone(), 1000); + state.latest_block_header.slot = 90; + state.latest_justified.slot = 90; + state.latest_justified.root = H256::from([99u8; 32]); // Wrong root + assert!(verify_checkpoint_state(&state, 1000, &validators).is_err()); + } +} diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index a906f71..c7b54b9 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -1,3 +1,4 @@ +mod checkpoint_sync; mod version; use std::{ @@ -20,7 +21,7 @@ use tracing::{error, info}; use tracing_subscriber::{EnvFilter, Layer, Registry, layer::SubscriberExt}; use ethlambda_blockchain::BlockChain; -use ethlambda_storage::{Store, backend::RocksDBBackend}; +use ethlambda_storage::{StorageBackend, Store, backend::RocksDBBackend}; const ASCII_ART: &str = r#" _ _ _ _ _ @@ -46,10 +47,14 @@ struct CliOptions { /// The node ID to look up in annotated_validators.yaml (e.g., "ethlambda_0") #[arg(long)] node_id: String, + /// URL of a peer to download checkpoint state from (e.g., http://peer:5052) + /// When set, skips genesis initialization and syncs from checkpoint. + #[arg(long)] + checkpoint_sync_url: Option, } #[tokio::main] -async fn main() { +async fn main() -> eyre::Result<()> { let filter = EnvFilter::builder() .with_default_directive(tracing::Level::INFO.into()) .from_env_lossy(); @@ -95,13 +100,18 @@ async fn main() { populate_name_registry(&validator_config); let bootnodes = read_bootnodes(&bootnodes_path); - let validators = genesis_config.validators(); let validator_keys = read_validator_keys(&validators_path, &validator_keys_dir, &options.node_id); - let genesis_state = State::from_genesis(genesis_config.genesis_time, validators); let backend = Arc::new(RocksDBBackend::open("./data").expect("Failed to open RocksDB")); - let store = Store::from_anchor_state(backend, genesis_state); + + let store = fetch_initial_state( + options.checkpoint_sync_url.as_deref(), + &genesis_config, + backend.clone(), + ) + .await + .inspect_err(|err| error!(%err, "Failed to initialize state"))?; let (p2p_tx, p2p_rx) = tokio::sync::mpsc::unbounded_channel(); let blockchain = BlockChain::spawn(store.clone(), p2p_tx, validator_keys); @@ -130,6 +140,8 @@ async fn main() { } } println!("Shutting down..."); + + Ok(()) } fn populate_name_registry(validator_config: impl AsRef) { @@ -257,3 +269,51 @@ fn read_hex_file_bytes(path: impl AsRef) -> Vec { }; bytes } + +/// Fetch the initial state for the node. +/// +/// If `checkpoint_url` is provided, performs checkpoint sync by downloading +/// and verifying the finalized state from a remote peer. Otherwise, creates +/// a genesis state from the local genesis configuration. +/// +/// # Arguments +/// +/// * `checkpoint_url` - Optional URL to fetch checkpoint state from +/// * `genesis` - Genesis configuration (for genesis_time verification and genesis state creation) +/// * `validators` - Validator set (moved for genesis state creation) +/// * `backend` - Storage backend for Store creation +/// +/// # Returns +/// +/// `Ok(Store)` on success, or `Err(CheckpointSyncError)` if checkpoint sync fails. +/// Genesis path is infallible and always returns `Ok`. +async fn fetch_initial_state( + checkpoint_url: Option<&str>, + genesis: &GenesisConfig, + backend: Arc, +) -> Result { + let validators = genesis.validators(); + + let Some(checkpoint_url) = checkpoint_url else { + info!("No checkpoint sync URL provided, initializing from genesis state"); + let genesis_state = State::from_genesis(genesis.genesis_time, validators); + return Ok(Store::from_anchor_state(backend, genesis_state)); + }; + + // Checkpoint sync path + info!(%checkpoint_url, "Starting checkpoint sync"); + + let state = + checkpoint_sync::fetch_checkpoint_state(checkpoint_url, genesis.genesis_time, &validators) + .await?; + + info!( + slot = state.slot, + validators = state.validators.len(), + finalized_slot = state.latest_finalized.slot, + "Checkpoint sync complete" + ); + + // Store the anchor state and header, without body + Ok(Store::from_anchor_state(backend, state)) +}