Skip to content

Conversation

@MegaRedHand
Copy link
Collaborator

@MegaRedHand MegaRedHand commented Feb 4, 2026

Closes #80
Closes #89

This PR adds checkpoint-sync functionality when --checkpoint-sync-url is specified. This means the node will fetch the latest finalized state from the URL specified in the flag.

NOTE: has some changes from #96, they'll be gone once that's merged.

Other changes in this PR

2162947: includes an optimization to the unknown block fetch mechanism in BlockChainServer. While testing checkpoint-sync we found the backfill never ended, so we sped it up a bit by storing the oldest missing ancestor of each block, jumping to it directly instead of going block by block and resetting after a failed request like now.

ebcc1a7: fixes block fetch logic in the P2P module. Before we considered empty responses as succeded, so we didn't retry. Now we follow the same logic as for error responses: we retry after a backoff.

89b5c3f: increases the number of retries to 10, with 5ms initial backoff and an exponent of 2.

@MegaRedHand MegaRedHand changed the title feat: add checkpoint-sync support feat: add checkpoint-sync Feb 4, 2026
@github-actions
Copy link

github-actions bot commented Feb 4, 2026

🤖 Kimi Code Review

Review Summary

This PR adds checkpoint sync functionality to ethlambda, allowing nodes to sync from a recent finalized state instead of genesis. The implementation is well-structured and follows Rust best practices, but there are several important issues to address:

Critical Issues

  1. Security: Missing TLS certificate validation (checkpoint_sync.rs:51-65)

    • The Client is built without certificate validation, making it vulnerable to MITM attacks
    • Fix: Add .use_rustls_tls() and proper certificate configuration
  2. Security: Missing state root verification (checkpoint_sync.rs:20-32)

    • The downloaded state isn't verified against a trusted state root
    • Fix: Add state root verification using a trusted checkpoint from config
  3. Consensus: Incomplete fork choice initialization (main.rs:336-338)

    • Store::get_forkchoice_store doesn't exist - this will fail to compile
    • Fix: Use proper initialization method or implement the missing function

Important Issues

  1. Error handling: Missing timeout handling (checkpoint_sync.rs:51-65)

    • Timeouts aren't properly handled - could hang indefinitely
    • Fix: Add explicit timeout handling and retry logic
  2. Resource management: No connection pooling (checkpoint_sync.rs:51-65)

    • New client created for each request - inefficient
    • Fix: Use a shared client instance
  3. Validation: Missing slot range check (checkpoint_sync.rs:125-129)

    • No check if checkpoint slot is reasonable (not too old/future)
    • Fix: Add slot range validation against current time

Minor Issues

  1. Code duplication: create_test_validator() and create_different_validator() could be parameterized
  2. Test coverage: Missing tests for HTTP error cases and timeout scenarios
  3. Documentation: Missing documentation for public functions in checkpoint_sync.rs

Line-specific Feedback

  • Line 51-65: Add TLS configuration
  • Line 125-129: Add slot range validation
  • Line 336: Fix Store::get_forkchoice_store call
  • Line 427: Add more comprehensive test cases for edge conditions

The PR is well-structured overall, but the security and consensus issues must be addressed before merging.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

🤖 Codex Code Review

Review Findings (ordered by severity)

  • Checkpoint sync can accept a forged state with no cryptographic anchoring. verify_checkpoint_state only checks genesis time and validator pubkeys, so a malicious peer can return an arbitrary state (balances, finality, etc.) that still passes. For checkpoint sync, you typically need a trusted checkpoint root (weak subjectivity) or at least verify the state root against a known finalized block root. Consider requiring an expected checkpoint root input (CLI flag) and verifying state.tree_hash_root() equals it, or verifying the downloaded state corresponds to a trusted root from config. bin/ethlambda/src/checkpoint_sync.rs:94-185

  • Missing consistency check between state root and latest_block_header.state_root. The state includes a latest_block_header with a state_root, but there is no verification that state.tree_hash_root() equals latest_block_header.state_root. Without this, the header can be unrelated to the state. Add a check to bind the state to its header. bin/ethlambda/src/checkpoint_sync.rs:165-183

  • Potential fork choice mismatch: anchor block uses empty body. construct_anchor_block uses BlockBody::default() even though the header’s body_root may differ, making Block::tree_hash_root() inconsistent with latest_block_header. If fork choice or storage uses the block root, this can cause incorrect anchoring or invalid justification links. Consider storing/using the header root directly, or constructing an “anchor block” type that preserves body_root semantics. bin/ethlambda/src/checkpoint_sync.rs:188-201, bin/ethlambda/src/main.rs:322-336

  • DoS risk: unbounded checkpoint response size. response.bytes().await? buffers the full state in memory without a maximum size. A peer can return a huge body and OOM the node. Add a maximum size guard (e.g., check Content-Length and enforce a cap, or stream into a bounded buffer). bin/ethlambda/src/checkpoint_sync.rs:76-84

Consensus-layer considerations

  • Fork choice anchoring correctness depends on the block root. With the current empty-body anchor, verify that Store::get_forkchoice_store does not depend on Block::tree_hash_root() for the anchor root. If it does, you need to store or pass the header root directly to avoid mismatch. bin/ethlambda/src/checkpoint_sync.rs:188-201, bin/ethlambda/src/main.rs:336

Rust/maintainability notes

  • The test coverage for structural checks is solid. Consider adding a test that explicitly checks the new “state root vs header state_root” invariant if you add it. bin/ethlambda/src/checkpoint_sync.rs:204-427

Open questions

  • Is /lean/v0/states/finalized guaranteed to return a state at the finalized slot? If yes, you should add state.slot == state.latest_finalized.slot (and probably state.latest_block_header.slot == state.slot) to prevent inconsistent checkpoints. bin/ethlambda/src/checkpoint_sync.rs:148-176

If you address the anchoring and validation gaps above, the rest of the changes look clean and well-structured.


Automated review by OpenAI Codex · custom prompt

@github-actions
Copy link

github-actions bot commented Feb 4, 2026

🤖 Claude Code Review

Perfect! Now I have a complete picture of the PR. Let me compile my review findings.

Code Review: PR #93 - Add Checkpoint Sync Support

Summary

This PR adds checkpoint sync functionality to allow nodes to sync from a trusted checkpoint state instead of starting from genesis. The implementation is well-structured with proper validation and error handling. Below are my findings.


✅ Strengths

  1. Comprehensive validation checks - The verify_checkpoint_state() function includes extensive safety checks for consensus-critical data
  2. Well-tested - 19 unit tests covering various validation scenarios
  3. Clean error handling - Uses thiserror for structured error types
  4. Proper timeout strategy - Uses read timeout instead of total timeout to handle large states gracefully
  5. Good separation of concerns - Checkpoint logic isolated in dedicated module
  6. Follows existing patterns - Reuses Store::get_forkchoice_store() correctly

🔴 Critical Issues

1. State Root Mismatch Not Validated (Security)

Location: bin/ethlambda/src/checkpoint_sync.rs:327

The construct_anchor_block() creates a block with a default empty body, but the function doesn't verify that the computed block root matches the checkpoint's finalized/justified root. This could allow accepting an invalid checkpoint state where the state root doesn't correspond to the actual state data.

Issue:

pub fn construct_anchor_block(state: &State) -> Block {
    Block {
        slot: state.latest_block_header.slot,
        parent_root: state.latest_block_header.parent_root,
        proposer_index: state.latest_block_header.proposer_index,
        state_root: state.latest_block_header.state_root,
        body: BlockBody::default(),  // ⚠️ Default body - root may not match!
    }
}

The block's tree_hash_root() will differ from the original if the original had a non-empty body. While the comment mentions this, there's no validation that the state is actually from the checkpoint we think it is.

Recommendation:

  • Compute the state root from the downloaded state: let computed_state_root = state.tree_hash_root()
  • Compare with state.latest_block_header.state_root to ensure integrity
  • Add this check to verify_checkpoint_state() as:
    // Verify state root integrity
    let computed_state_root = state.tree_hash_root();
    if computed_state_root != state.latest_block_header.state_root {
        return Err(CheckpointSyncError::StateRootMismatch { 
            expected: state.latest_block_header.state_root, 
            got: computed_state_root 
        });
    }

2. No HTTPS Enforcement (Security)

Location: bin/ethlambda/src/checkpoint_sync.rs:69-73

Checkpoint sync downloads consensus state over HTTP without enforcing HTTPS. An attacker performing a MITM attack could provide a malicious state that passes validation checks.

Current code:

let url = format!("{base_url}/lean/v0/states/finalized");
let client = Client::builder()
    .connect_timeout(CHECKPOINT_CONNECT_TIMEOUT)
    .read_timeout(CHECKPOINT_READ_TIMEOUT)
    .build()?;

Recommendation:

  • Enforce HTTPS scheme in the URL validation
  • Add a check like:
    if !base_url.starts_with("https://") {
        return Err(CheckpointSyncError::InsecureUrl);
    }
  • Consider adding a --allow-insecure-checkpoint flag for development/testing

3. Potential Type Confusion in Checkpoint Validation

Location: bin/ethlambda/src/checkpoint_sync.rs:172-182

The validation checks if latest_block_header matches finalized/justified roots, but uses .tree_hash_root() on the header, not the full block. This could lead to false positives/negatives.

let block_root = state.latest_block_header.tree_hash_root();  // Header root

if state.latest_block_header.slot == state.latest_finalized.slot
    && block_root != state.latest_finalized.root.0  // Comparing to block root?
{
    return Err(CheckpointSyncError::BlockHeaderFinalizedRootMismatch);
}

Question: Does state.latest_finalized.root store a block root or a header root? If it's a block root, this comparison is incorrect.

Investigation needed: Check the consensus spec to confirm what Checkpoint.root represents. If it's a block root, this validation is broken.


⚠️ High Priority Issues

4. Missing Slot Bounds Check

Location: bin/ethlambda/src/checkpoint_sync.rs:147-149

While we check finalized_slot <= state.slot, we don't validate that the checkpoint isn't from the future relative to system time.

Recommendation:

// Verify checkpoint slot is not in the future
let current_time = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)
    .unwrap()
    .as_secs();
let max_slot = (current_time - state.config.genesis_time) / SECONDS_PER_SLOT;
if state.slot > max_slot + 10 {  // Allow 10 slot tolerance
    return Err(CheckpointSyncError::CheckpointTooFarInFuture);
}

5. No Size Limit on Downloaded State

Location: bin/ethlambda/src/checkpoint_sync.rs:82

The code downloads the entire state into memory without any size checks:

let bytes = response.bytes().await?;

A malicious or misconfigured peer could send gigabytes of data, causing OOM.

Recommendation:

  • Add a reasonable size limit (e.g., 100MB)
  • Use response.content_length() to check before download:
    if let Some(len) = response.content_length() {
        if len > MAX_STATE_SIZE {
            return Err(CheckpointSyncError::StateTooLarge);
        }
    }

6. Error Context Lost in Main

Location: bin/ethlambda/src/main.rs:110-111

The error is logged but all context is lost before exit:

.inspect_err(|err| error!(%err, "Failed to initialize state"))
.unwrap_or_else(|_| std::process::exit(1));

The unwrap_or_else closure receives |_|, discarding the error. While inspect_err logs it, this pattern is fragile.

Recommendation:

.unwrap_or_else(|err| {
    error!(%err, "Failed to initialize state");
    std::process::exit(1)
})

💡 Medium Priority Issues

7. Inconsistent Checkpoint Root Field Access

Location: bin/ethlambda/src/checkpoint_sync.rs:172,178

Sometimes accessing state.latest_finalized.root.0 (inner field) and sometimes state.latest_finalized.root (the H256 wrapper):

&& block_root != state.latest_finalized.root.0  // Line 172 - accessing .0
...
&& state.latest_justified.root != state.latest_finalized.root  // Line 158 - direct comparison

Fix: Be consistent - use direct H256 comparison everywhere since tree_hash_root() returns H256:

if ... && H256(block_root) != state.latest_finalized.root {

Or keep the block_root as [u8; 32] if that's the intent.


8. Missing Validator Epoch Checks

Location: bin/ethlambda/src/checkpoint_sync.rs:134-144

The validator pubkey validation is good, but doesn't check for epoch boundaries or activation/exit epochs. While this may be simplified for lean consensus, ensure validators in the checkpoint are actually active.


9. Construct Anchor Block Creates Misleading Data

Location: bin/ethlambda/src/checkpoint_sync.rs:188-197

The comment acknowledges the issue:

IMPORTANT: This creates a block with default body. The block's tree_hash_root() will only match the original block if the original also had an empty body.

This is dangerous because:

  • The anchor block's hash won't match the actual block hash
  • Fork choice relies on block roots being correct
  • Could cause issues during block requests by root

Concern: Will fork choice break if we ask peers for this block by its (incorrect) computed root?

Recommendation: Document why this is safe, or fetch the actual block body from the peer.


🔧 Minor Issues / Style

10. Redundant URL Trimming

Location: bin/ethlambda/src/checkpoint_sync.rs:68

let base_url = base_url.trim_end_matches('/');
let url = format!("{base_url}/lean/v0/states/finalized");

Minor nit: works correctly but adds complexity. Consider documenting expected URL format.


11. Magic Numbers

Location: bin/ethlambda/src/checkpoint_sync.rs:9-13

const CHECKPOINT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
const CHECKPOINT_READ_TIMEOUT: Duration = Duration::from_secs(15);

Good to have constants, but 15 seconds might be aggressive for connect timeout in poor network conditions. Consider making configurable or bumping to 30s.


12. Test Coverage Gaps

The tests are comprehensive for validation logic but missing:

  • Integration test for actual HTTP download
  • Test for SSZ deserialization failures
  • Test for malformed HTTP responses
  • Test for timeout scenarios

13. Logging Could Be More Informative

Location: bin/ethlambda/src/main.rs:320-334

Good logging, but consider adding:

  • Checkpoint root hash (finalized/justified)
  • Time taken to download
  • State size in MB
  • URL being used (already logged, good!)

🎯 Rust Best Practices

14. Good Use of From/Into Traits

The codebase properly uses From implementations for ResponseCode conversions elsewhere, and this PR follows suit.

15. Proper Ownership

The validators vec is moved into fetch_initial_state, avoiding unnecessary clones. Good!

16. Error Handling with inspect_err

Follows project patterns correctly.


📋 Architecture Questions

  1. What happens to existing RocksDB data? If a node has existing state and then starts with --checkpoint-sync-url, will it overwrite? Should we check if DB is empty first?

  2. Recovery strategy: If checkpoint sync fails mid-download, do we fall back to genesis or fail completely? Current behavior: fail and exit (line 111).

  3. Checkpoint age: Should we warn if the checkpoint is very old (e.g., >1 week behind current slot)?


Summary Assessment

Category Status
Correctness ⚠️ Issues #1, #3 need investigation
Security 🔴 Issues #1, #2 are critical
Performance ✅ Good timeout strategy
Memory Safety ⚠️ Issue #5 (unbounded download)
Error Handling ✅ Well-structured
Testing ✅ Comprehensive unit tests
Code Quality ✅ Clean, readable

Recommendations Priority

  1. MUST FIX: State root validation (docs: add initial roadmap #1)
  2. MUST FIX: HTTPS enforcement (chore: initial repo setup #2)
  3. MUST INVESTIGATE: Checkpoint root type confusion (docs: improve readme #3)
  4. SHOULD FIX: Download size limit (feat: load genesis network configuration and state #5)
  5. SHOULD FIX: Future slot check (docs: add philosophy and design principles #4)
  6. CONSIDER: Anchor block root mismatch implications (feat: listen for gossipsub blocks #9)

The core implementation is solid, but the consensus-critical validation needs strengthening before merge.


Automated review by Claude (Anthropic) · custom prompt

pablodeymo pushed a commit that referenced this pull request Feb 9, 2026
Required for #93

This PR streamlines block storage by splitting block headers from
bodies. This simplifies the handling of the anchor state, since the
block header is readily available from it, but the body is not, even if
it's unnecessary for child block processing.
@MegaRedHand MegaRedHand marked this pull request as ready for review February 10, 2026 20:39
@github-actions
Copy link

🤖 Kimi Code Review

Review Summary

Overall Assessment: The checkpoint sync implementation is well-structured with comprehensive validation. However, there are several critical security and correctness issues that need addressing.

Critical Issues

  1. Missing SSZ Size Limits (bin/ethlambda/src/checkpoint_sync.rs:52-54)

    • No validation of response size before deserialization. A malicious peer could send a 10GB+ payload causing DoS.
    • Fix: Add size limit check before deserialization:
    const MAX_STATE_SIZE: usize = 100 * 1024 * 1024; // 100MB
    if bytes.len() > MAX_STATE_SIZE {
        return Err(CheckpointSyncError::StateTooLarge(bytes.len()));
    }
  2. HTTP Security Issues (bin/ethlambda/src/checkpoint_sync.rs:42-55)

    • No TLS certificate validation disabled (using rustls-tls but no verification)
    • Fix: Configure certificate verification or add explicit --insecure flag
  3. Missing Fork Version Check (bin/ethlambda/src/checkpoint_sync.rs:verify_checkpoint_state)

    • No validation of state.config.fork_version against local config
    • Fix: Add fork version comparison similar to genesis time check

High Priority Issues

  1. Incomplete Checkpoint Validation (bin/ethlambda/src/checkpoint_sync.rs:verify_checkpoint_state)

    • Missing validation of state.config fields (min_validator_balance, max_validator_balance, etc.)
    • Missing validation of validator withdrawal credentials
    • Fix: Add comprehensive config field validation
  2. State Transition Edge Cases (bin/ethlambda/src/checkpoint_sync.rs:verify_checkpoint_state)

    • No validation that latest_block_header.state_root matches actual state root
    • Fix: Add check: state.latest_block_header.state_root == state.tree_hash_root()

Medium Priority Issues

  1. Error Handling (bin/ethlambda/src/main.rs:114-116)

    • unwrap_or_else usage is non-idiomatic for async context
    • Fix: Use proper error handling:
    match fetch_initial_state(...).await {
        Ok(store) => store,
        Err(e) => {
            error!(%e, "Failed to initialize state");
            std::process::exit(1);
        }
    }
  2. URL Construction (bin/ethlambda/src/checkpoint_sync.rs:42)

    • URL construction could be cleaner with Url crate
    • Fix: Use Url::join() for safer URL building
  3. Test Coverage (bin/ethlambda/src/checkpoint_sync.rs:tests)

    • Missing tests for SSZ decode failures
    • Missing tests for HTTP timeout scenarios
    • Fix: Add mock HTTP server tests

Minor Issues

  1. Documentation (bin/ethlambda/src/checkpoint_sync.rs:verify_checkpoint_state)

    • Function docstring mentions validators parameter but it's not in signature
    • Fix: Update docstring to match actual parameters
  2. Code Organization (bin/ethlambda/src/main.rs:fetch_initial_state)

    • Function signature comment mentions validators parameter that doesn't exist
    • Fix: Update documentation

Security Recommendations

  • Add rate limiting for checkpoint sync attempts
  • Consider adding checkpoint state signature verification (if available)
  • Add logging for all validation failures for debugging

The implementation is solid but needs the above security and correctness fixes before production use.


Automated review by Kimi (Moonshot AI) · custom prompt

@github-actions
Copy link

🤖 Codex Code Review

Findings

  • High: Unbounded download + full in‑memory buffer. response.bytes().await? reads the entire state with no cap or Content-Length validation, which is a straightforward OOM/DoS vector from a malicious peer. Consider streaming with a hard size limit and rejecting oversized bodies. bin/ethlambda/src/checkpoint_sync.rs:82.
  • High: Missing binding between the downloaded state and the header’s state_root. The verification only compares latest_block_header roots to justified/finalized roots when slots match, but never checks state.latest_block_header.state_root == state.tree_hash_root() (and ideally latest_block_header.slot == state.slot). A peer can serve an arbitrary state that passes validator/genesis checks but is not the state committed by the header. bin/ethlambda/src/checkpoint_sync.rs:165-176.
  • Medium: Checkpoint sync accepts cleartext HTTP without any warning or opt‑in. This enables MITM state poisoning on untrusted networks. Consider requiring HTTPS by default or logging a prominent warning and a --allow-insecure flag. bin/ethlambda/src/checkpoint_sync.rs:68-82 (URL use), bin/ethlambda/src/main.rs (CLI option).

Questions / assumptions

  • This assumes validator sets are static and must exactly match genesis. If validator activation/deactivation is expected, the strict validator list check will fail checkpoint sync. Is that intended for this lean spec?

If you address the two high‑severity items, the rest looks reasonable for the minimal checkpoint‑sync flow.


Automated review by OpenAI Codex · custom prompt

@github-actions
Copy link

🤖 Claude Code Review

Now let me compile my findings into a comprehensive review.

PR #93 Review: Checkpoint Sync Implementation

Summary

This PR adds checkpoint sync functionality via --checkpoint-sync-url flag. The implementation is solid with comprehensive validation and excellent test coverage. Found one critical bug and several improvement opportunities.


Critical Issues

🔴 State Root Comparison Bug (Lines 172-181)

Location: bin/ethlambda/src/checkpoint_sync.rs:170-181

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);
}

Problem: Computing tree_hash_root() on latest_block_header will include whatever state_root value is currently in the header. However, looking at Store::init_store() (lines 180-195 in store.rs), the proper verification flow is:

  1. Save original state_root
  2. Zero it out before computing block root
  3. Verify the original matches the computed state root (or is zero for genesis)

Why this matters: If a malicious peer sends a checkpoint state with an incorrect state_root in the block header, this verification will pass as long as the overall block root (including the bad state_root) matches. The actual state root verification happens later in init_store, but by then you've already accepted the state as valid.

Fix:

// Save original state_root for proper validation
let original_state_root = state.latest_block_header.state_root;

// Compute block root with zeroed state_root (matches what init_store does)
let mut header_for_root = state.latest_block_header.clone();
header_for_root.state_root = H256::ZERO;
let block_root = header_for_root.tree_hash_root();

// Verify state_root is either zero (shouldn't be for checkpoint) or computed value
// This prevents accepting states with arbitrary state_root values
if original_state_root != H256::ZERO {
    let mut state_for_root = state.clone();
    state_for_root.latest_block_header.state_root = H256::ZERO;
    let computed_state_root = state_for_root.tree_hash_root();
    if original_state_root != computed_state_root {
        return Err(CheckpointSyncError::StateRootMismatch { 
            expected: computed_state_root, 
            got: original_state_root 
        });
    }
}

// Now verify block root matches checkpoints
if state.latest_block_header.slot == state.latest_finalized.slot
    && block_root != state.latest_finalized.root.0
{ ... }

Impact: Medium-High security risk. While init_store will eventually catch this, verification should be complete before accepting the state.


Medium Priority Issues

⚠️ Missing State Root Verification (Lines 87-185)

The verification function doesn't explicitly validate that state.latest_block_header.state_root matches the actual state root. While init_store performs this check (line 189-192), it's better to fail early during verify_checkpoint_state.

Recommendation: Add explicit state root verification as shown in the fix above. Add a new error variant:

#[error("state root mismatch: expected {expected}, got {got}")]
StateRootMismatch { expected: H256, got: H256 },

⚠️ No Verification of justified_slots or justifications_* Fields

Location: Lines 87-185

The verification doesn't check:

  • state.justified_slots - should be properly initialized relative to finalized_slot
  • state.justifications_roots - historical justification data
  • state.justifications_validators - validator participation tracking

Question: Are these fields expected to be populated in a checkpoint state, or are they rebuild-able from block headers? Based on the test fixture (line 219), they're initialized to zero/empty. If they should contain data, add verification. If they're optional, document why they're not checked.

⚠️ HTTP Security: No HTTPS Enforcement

Location: bin/ethlambda/src/checkpoint_sync.rs:65-84

The code accepts any URL scheme. For production, checkpoint sync should require HTTPS to prevent MITM attacks during initial sync.

Recommendation:

pub async fn fetch_checkpoint_state(base_url: &str) -> Result<State, CheckpointSyncError> {
    let base_url = base_url.trim_end_matches('/');
    
    // Enforce HTTPS for security (except localhost for testing)
    if !base_url.starts_with("https://") 
        && !base_url.starts_with("http://localhost")
        && !base_url.starts_with("http://127.0.0.1") {
        return Err(CheckpointSyncError::InsecureUrl);
    }
    
    let url = format!("{base_url}/lean/v0/states/finalized");
    // ... rest
}

Minor Issues / Suggestions

📝 Consider Response Size Limits

Location: Line 82 let bytes = response.bytes().await?;

No size limit on the response body. A malicious peer could send a multi-GB payload. Consider adding:

.body_stream_max_length(Some(50 * 1024 * 1024)) // 50MB max

Or manually check Content-Length header before downloading.

📝 Improve Error Context

Location: Line 83

Add context about which URL failed:

let state = State::from_ssz_bytes(&bytes)
    .map_err(|e| {
        error!(url = %url, byte_count = bytes.len(), "SSZ decode failed");
        CheckpointSyncError::SszDecode(e)
    })?;

📝 Inconsistent Terminology: "Checkpoint" vs "Finalized"

The function is named fetch_checkpoint_state but the endpoint is /states/finalized. Are these always the same? Consider documenting this relationship or renaming for clarity.

📝 Missing Historical Roots Validation

Location: Line 87-185

The state.historical_block_hashes field isn't verified. Should it contain exactly the expected historical block roots up to the finalized slot? Or is it okay for it to be empty/partial?


Good Practices Observed ✅

  1. Excellent test coverage - 19 unit tests covering all validation branches
  2. Proper timeout strategy - Connect timeout + read timeout (not total timeout) is correct for large downloads
  3. Comprehensive validation - Genesis time, validator set, checkpoint consistency all checked
  4. Good error types - Descriptive errors with context
  5. Proper use of inspect_err - Following project conventions (line 114 in main.rs)
  6. Documentation - Clear comments explaining timeout strategy and verification purpose

Consensus-Layer Specific Observations

  1. Justification/Finalization Logic: The checks on lines 147-162 correctly enforce:

    • finalized_slot ≤ state.slot
    • justified_slot ≥ finalized_slot
    • If justified_slot == finalized_slot, roots must match
  2. Sequential Validator Indices: Lines 124-132 properly enforce sequential indexing (0, 1, 2, ...), which is critical for the validator duties assignment.

  3. Store Integration: The use of Store::from_anchor_state() is correct. The store's init_store() function handles state root computation and validation properly.


Testing Recommendations

  1. Add integration test with a real HTTP server returning valid/invalid states
  2. Test timeout scenarios - stalled connections, slow downloads
  3. Test malformed SSZ - truncated data, invalid length prefixes
  4. Test state root mismatch scenarios once the fix is applied

Verdict

Approve with required changes. The implementation is well-structured with great test coverage, but the state root verification bug should be fixed before merge. The other security improvements (HTTPS enforcement, size limits) are recommended but not blocking for initial merge.


Automated review by Claude (Anthropic) · custom prompt

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 10, 2026

Greptile Overview

Greptile Summary

Implements checkpoint sync functionality allowing nodes to start from a recent finalized state instead of syncing from genesis. When --checkpoint-sync-url is specified, the node fetches the latest finalized state via HTTP from a remote peer, performs comprehensive validation checks, and initializes the store with that state.

Key changes:

  • New checkpoint_sync.rs module with fetch_checkpoint_state() for HTTP download and verify_checkpoint_state() for validation
  • Extensive validation: genesis time, validator set consistency, checkpoint ordering, block header consistency
  • Added --checkpoint-sync-url CLI flag to main.rs with conditional initialization logic
  • 17 comprehensive unit tests covering all validation scenarios
  • Uses reqwest with connection and read timeouts (15s each) for reliable downloads
  • Graceful fallback to genesis initialization when checkpoint URL not provided

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is well-structured with comprehensive validation, extensive test coverage (17 unit tests), proper error handling, clear documentation, and sensible timeout configurations. The checkpoint sync feature is optional and gracefully falls back to genesis initialization.
  • No files require special attention

Important Files Changed

Filename Overview
bin/ethlambda/src/checkpoint_sync.rs New checkpoint sync module with HTTP fetch, SSZ deserialization, and comprehensive validation logic with extensive test coverage
bin/ethlambda/src/main.rs Added checkpoint sync CLI flag and fetch_initial_state function to support downloading state from remote peer or genesis initialization
Cargo.toml Added reqwest HTTP client dependency with rustls-tls feature for checkpoint sync functionality
bin/ethlambda/Cargo.toml Added reqwest and thiserror dependencies for checkpoint sync module

Sequence Diagram

sequenceDiagram
    participant Main as main()
    participant FIS as fetch_initial_state()
    participant CS as checkpoint_sync
    participant HTTP as Remote Peer
    participant Store as Store
    
    Main->>FIS: checkpoint_sync_url provided?
    
    alt Checkpoint Sync Path
        FIS->>CS: fetch_checkpoint_state(url)
        CS->>HTTP: GET /lean/v0/states/finalized
        HTTP-->>CS: SSZ-encoded State bytes
        CS->>CS: Deserialize State from SSZ
        CS-->>FIS: Return State
        
        FIS->>CS: verify_checkpoint_state(state, genesis_time, validators)
        CS->>CS: Validate slot > 0
        CS->>CS: Validate validators exist
        CS->>CS: Check genesis_time matches
        CS->>CS: Check validator count matches
        CS->>CS: Verify validator indices sequential
        CS->>CS: Verify validator pubkeys match
        CS->>CS: Validate checkpoint consistency
        CS->>CS: Validate block header consistency
        CS-->>FIS: Validation OK
        
        FIS->>Store: from_anchor_state(backend, state)
        Store-->>FIS: Initialized Store
    else Genesis Path
        FIS->>FIS: State::from_genesis(genesis_time, validators)
        FIS->>Store: from_anchor_state(backend, genesis_state)
        Store-->>FIS: Initialized Store
    end
    
    FIS-->>Main: Return Store
Loading

Backfill is too slow to sync after a checkpoint-sync. This should
make it fast enough.
Also tunes parameters so added retry events fall between the old ones.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve pending block fetching from P2P Add checkpoint sync

1 participant