Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 320 additions & 38 deletions src/lean_spec/__main__.py

Large diffs are not rendered by default.

72 changes: 59 additions & 13 deletions src/lean_spec/subspecs/api/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
"""
Checkpoint sync client for downloading finalized state from another node.

This client is used for fast synchronization - instead of syncing from genesis,
a node can download the finalized state from a trusted peer and start from there.
Checkpoint sync enables fast startup by skipping historical block processing.
Instead of replaying every block from genesis, a node downloads a recent
finalized state and starts from there.

Trust model:

- The operator trusts the checkpoint source to provide valid finalized state
- This trust is acceptable because finalized state has 2/3 validator support
- The alternative (genesis sync) may take hours or days on mainnet

The trade-off is trustlessness for speed. Most operators accept this because
they already trust their checkpoint source (often their own infrastructure
or a well-known provider).
"""

from __future__ import annotations
Expand All @@ -21,36 +32,49 @@

logger = logging.getLogger(__name__)

# Constants
DEFAULT_TIMEOUT = 60.0
"""HTTP request timeout in seconds. Large states may take time to transfer."""

FINALIZED_STATE_ENDPOINT = "/lean/states/finalized"
"""API endpoint for fetching finalized state. Follows Beacon API conventions."""


class CheckpointSyncError(Exception):
"""Error during checkpoint sync."""
"""
Error during checkpoint sync.

Raised when the checkpoint state cannot be fetched or is invalid.
Callers should handle this by aborting startup (not falling back).
"""


async def fetch_finalized_state(url: str, state_class: type[Any]) -> "State":
"""
Fetch finalized state from a node via checkpoint sync.

Downloads the finalized state as SSZ binary and deserializes it.
Downloads the state as SSZ binary and deserializes it. SSZ format is
preferred over JSON because state objects are large (tens of MB) and
SSZ is more compact and faster to parse.

Args:
url: Base URL of the node API (e.g., "http://localhost:5052")
state_class: The State class to deserialize into
url: Base URL of the node API (e.g., "http://localhost:5052").
state_class: The State class to deserialize into.

Returns:
The finalized State object
The finalized State object.

Raises:
CheckpointSyncError: If the request fails or state is invalid
CheckpointSyncError: If the request fails or state is invalid.
"""
base_url = url.rstrip("/")
full_url = f"{base_url}{FINALIZED_STATE_ENDPOINT}"

logger.info(f"Fetching finalized state from {full_url}")

# Request SSZ binary format.
#
# The Accept header tells the server we want raw bytes, not JSON.
# This is faster to transfer and parse than JSON encoding.
headers = {"Accept": "application/octet-stream"}

try:
Expand All @@ -61,6 +85,10 @@ async def fetch_finalized_state(url: str, state_class: type[Any]) -> "State":
ssz_data = response.content
logger.info(f"Downloaded {len(ssz_data)} bytes of SSZ state data")

# Deserialize from SSZ bytes.
#
# This validates the byte stream matches the expected schema.
# Malformed data will raise an exception here.
state = state_class.decode_bytes(ssz_data)
logger.info(f"Deserialized state at slot {state.slot}")

Expand All @@ -80,33 +108,51 @@ async def fetch_finalized_state(url: str, state_class: type[Any]) -> "State":

async def verify_checkpoint_state(state: "State") -> bool:
"""
Verify that a checkpoint state is valid.
Verify that a checkpoint state is structurally valid.

This is defense-in-depth validation. We trust the checkpoint source,
but still verify basic invariants before using the state. These checks
catch corrupted downloads or misconfigured servers.

The checks are intentionally minimal:

- Slot is non-negative (sanity check)
- Validators exist (empty state is useless)
- Validator count within limits (prevents DoS)

Performs basic validation checks on the downloaded state.
We do NOT verify cryptographic proofs here. That would require
the full block history, defeating the purpose of checkpoint sync.

Args:
state: The state to verify
state: The state to verify.

Returns:
True if valid, False otherwise
True if valid, False otherwise.
"""
try:
# Sanity check: slot must be non-negative.
if state.slot < Slot(0):
logger.error("Invalid state: negative slot")
return False

# A state with no validators cannot produce blocks.
validator_count = len(state.validators)
if validator_count == 0:
logger.error("Invalid state: no validators")
return False

# Guard against oversized states that could exhaust memory.
if validator_count > int(DEVNET_CONFIG.validator_registry_limit):
logger.error(
f"Invalid state: validator count {validator_count} exceeds "
f"registry limit {DEVNET_CONFIG.validator_registry_limit}"
)
return False

# Compute state root to verify SSZ deserialization worked correctly.
#
# If the data was corrupted, hashing will likely fail or produce
# an unexpected result. We log the root for debugging.
state_root = hash_tree_root(state)
root_preview = state_root.hex()[:16]
logger.info(f"Checkpoint state verified: slot={state.slot}, root={root_preview}...")
Expand Down
Loading
Loading