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
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ uvx tox # Everything (checks + tests + docs)

### Import Style

**All imports must be at the top of the file.** Never place imports inside functions, methods, or conditional blocks. This applies to both source code and tests. If a circular dependency exists, restructure the code to break the cycle rather than using a lazy import.

Bad:
```python
def process(data):
from lean_spec.subspecs.ssz import hash_tree_root
return hash_tree_root(data)
```

Good:
```python
from lean_spec.subspecs.ssz import hash_tree_root

def process(data):
return hash_tree_root(data)
```

**Avoid confusing import renames.** When an external library exports a name that conflicts with a local type, prefer restructuring over renaming.

Bad:
Expand Down
22 changes: 9 additions & 13 deletions src/lean_spec/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
import argparse
import asyncio
import logging
import os
import sys
import time
from pathlib import Path

from lean_spec.subspecs.chain.config import ATTESTATION_COMMITTEE_COUNT
Expand All @@ -35,10 +38,16 @@
from lean_spec.subspecs.forkchoice import Store
from lean_spec.subspecs.genesis import GenesisConfig
from lean_spec.subspecs.networking.client import LiveNetworkEventSource
from lean_spec.subspecs.networking.enr import ENR
from lean_spec.subspecs.networking.gossipsub import GossipTopic
from lean_spec.subspecs.networking.reqresp.message import Status
from lean_spec.subspecs.node import Node, NodeConfig, get_local_validator_id
from lean_spec.subspecs.ssz.hash import hash_tree_root
from lean_spec.subspecs.sync.checkpoint_sync import (
CheckpointSyncError,
fetch_finalized_state,
verify_checkpoint_state,
)
from lean_spec.subspecs.validator import ValidatorRegistry
from lean_spec.types import Bytes32, Uint64

Expand Down Expand Up @@ -83,8 +92,6 @@ def resolve_bootnode(bootnode: str) -> str:
ValueError: If ENR is malformed or has no UDP connection info.
"""
if is_enr_string(bootnode):
from lean_spec.subspecs.networking.enr import ENR

enr = ENR.from_string(bootnode)

# Verify structural validity (correct scheme, public key present).
Expand Down Expand Up @@ -238,12 +245,6 @@ async def _init_from_checkpoint(
Returns:
A fully initialized Node if successful, None if checkpoint sync failed.
"""
from lean_spec.subspecs.sync.checkpoint_sync import (
CheckpointSyncError,
fetch_finalized_state,
verify_checkpoint_state,
)

try:
logger.info("Fetching checkpoint state from %s", checkpoint_sync_url)
state = await fetch_finalized_state(checkpoint_sync_url, State)
Expand Down Expand Up @@ -413,8 +414,6 @@ async def run_node(
genesis_time_now: Override genesis time to current time for testing.
is_aggregator: Enable aggregator mode for attestation aggregation.
"""
import time

logger.info("Loading genesis from %s", genesis_path)
genesis = GenesisConfig.from_yaml_file(genesis_path)

Expand Down Expand Up @@ -691,9 +690,6 @@ def main() -> None:
finally:
# Force exit to ensure all threads/sockets are released.
# This is important for QUIC which may have background threads.
import os
import sys

sys.stdout.flush()
sys.stderr.flush()
os._exit(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@

from __future__ import annotations

from typing import TYPE_CHECKING

from lean_spec.subspecs.chain.config import VALIDATOR_REGISTRY_LIMIT
from lean_spec.subspecs.containers.validator import ValidatorIndex, ValidatorIndices
from lean_spec.types import Boolean
from lean_spec.types.bitfields import BaseBitlist

if TYPE_CHECKING:
from lean_spec.subspecs.containers.validator import ValidatorIndex, ValidatorIndices


class AggregationBits(BaseBitlist):
"""
Expand Down Expand Up @@ -40,9 +36,6 @@ def from_validator_indices(
AssertionError: If no indices are provided.
AssertionError: If any index is outside the supported LIMIT.
"""
# Import here to avoid circular dependency
from lean_spec.subspecs.containers.validator import ValidatorIndices

# Extract list from ValidatorIndices if needed
index_list = indices.data if isinstance(indices, ValidatorIndices) else indices

Expand Down Expand Up @@ -74,9 +67,6 @@ def to_validator_indices(self) -> "ValidatorIndices":
Raises:
AssertionError: If no bits are set.
"""
# Import here to avoid circular dependency
from lean_spec.subspecs.containers.validator import ValidatorIndex, ValidatorIndices

# Extract indices where bit is set; fail if none found.
indices = [ValidatorIndex(i) for i, bit in enumerate(self.data) if bool(bit)]
if not indices:
Expand Down
3 changes: 1 addition & 2 deletions src/lean_spec/subspecs/networking/client/event_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
)
from lean_spec.subspecs.networking.transport import PeerId
from lean_spec.subspecs.networking.transport.connection import ConnectionManager, Stream
from lean_spec.subspecs.networking.transport.identity import IdentityKeypair
from lean_spec.subspecs.networking.transport.multistream import (
NegotiationError,
negotiate_server,
Expand Down Expand Up @@ -703,8 +704,6 @@ async def create(
Initialized event source.
"""
if connection_manager is None:
from lean_spec.subspecs.networking.transport.identity import IdentityKeypair

identity_key = IdentityKeypair.generate()
connection_manager = await ConnectionManager.create(identity_key)

Expand Down
13 changes: 3 additions & 10 deletions src/lean_spec/subspecs/networking/discovery/handshake.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
from dataclasses import dataclass, field
from enum import Enum, auto
from threading import Lock
from typing import TYPE_CHECKING

from lean_spec.types import Bytes32, Bytes33, Bytes64
from lean_spec.subspecs.networking.enr import ENR
from lean_spec.types import Bytes32, Bytes33, Bytes64, Uint64, rlp

from .config import HANDSHAKE_TIMEOUT_SECS
from .crypto import (
Expand All @@ -41,6 +41,7 @@
verify_id_nonce_signature,
)
from .keys import derive_keys_from_pubkey
from .messages import PROTOCOL_ID, PROTOCOL_VERSION, PacketFlag
from .packet import (
HandshakeAuthdata,
WhoAreYouAuthdata,
Expand All @@ -50,9 +51,6 @@
)
from .session import Session, SessionCache

if TYPE_CHECKING:
from lean_spec.subspecs.networking.enr import ENR


class HandshakeState(Enum):
"""Handshake state machine states."""
Expand Down Expand Up @@ -216,8 +214,6 @@ def create_whoareyou(
- nonce: The request_nonce to use in the packet header
- challenge_data: Full data for key derivation (masking-iv || static-header || authdata)
"""
from .messages import PROTOCOL_ID, PROTOCOL_VERSION, PacketFlag

id_nonce = generate_id_nonce()
authdata = encode_whoareyou_authdata(bytes(id_nonce), remote_enr_seq)

Expand Down Expand Up @@ -527,9 +523,6 @@ def _parse_enr_rlp(self, enr_rlp: bytes) -> "ENR | None":
Returns:
Parsed ENR with computed node ID, or None if malformed.
"""
from lean_spec.subspecs.networking.enr import ENR
from lean_spec.types import Bytes64, Uint64, rlp

try:
# Decode the RLP list structure.
#
Expand Down
10 changes: 4 additions & 6 deletions src/lean_spec/subspecs/networking/discovery/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@
import hashlib
import hmac

from Crypto.Hash import keccak

from lean_spec.types import Bytes16, Bytes32, Bytes33

from .crypto import ecdh_agree, pubkey_to_uncompressed

DISCV5_KEY_AGREEMENT_INFO = b"discovery v5 key agreement"
"""Info string used in HKDF expansion for Discovery v5 key derivation."""

Expand Down Expand Up @@ -134,8 +138,6 @@ def derive_keys_from_pubkey(
- send_key: Use to encrypt outgoing messages.
- recv_key: Use to decrypt incoming messages.
"""
from .crypto import ecdh_agree

# Compute shared secret.
secret = ecdh_agree(local_private_key, remote_public_key)

Expand Down Expand Up @@ -170,10 +172,6 @@ def compute_node_id(public_key_bytes: bytes) -> Bytes32:
Returns:
32-byte node ID.
"""
from Crypto.Hash import keccak

from .crypto import pubkey_to_uncompressed

# Ensure uncompressed format.
uncompressed = pubkey_to_uncompressed(public_key_bytes)

Expand Down
8 changes: 2 additions & 6 deletions src/lean_spec/subspecs/networking/discovery/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
import os
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable
from typing import Callable

from lean_spec.subspecs.networking.enr import ENR
from lean_spec.subspecs.networking.types import NodeId, SeqNumber
from lean_spec.types.uint import Uint8

Expand All @@ -42,9 +43,6 @@
from .session import BondCache
from .transport import DiscoveryTransport

if TYPE_CHECKING:
from lean_spec.subspecs.networking.enr import ENR

logger = logging.getLogger(__name__)

LOOKUP_PARALLELISM = ALPHA
Expand Down Expand Up @@ -639,8 +637,6 @@ def _process_discovered_enr(
enr_bytes: RLP-encoded ENR bytes from NODES response.
seen: Dict tracking nodes seen during current lookup.
"""
from lean_spec.subspecs.networking.enr import ENR

try:
# Parse ENR from RLP.
enr = ENR.from_rlp(enr_bytes)
Expand Down
20 changes: 8 additions & 12 deletions src/lean_spec/subspecs/networking/enr/enr.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@
import base64
from typing import ClassVar, Self

from Crypto.Hash import keccak
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
Prehashed,
encode_dss_signature,
)

from lean_spec.subspecs.networking.types import Multiaddr, NodeId, SeqNumber
from lean_spec.types import (
Bytes32,
Expand Down Expand Up @@ -237,14 +245,6 @@ def verify_signature(self) -> bool:

Returns True if signature is valid, False otherwise.
"""
from Crypto.Hash import keccak
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
Prehashed,
encode_dss_signature,
)

if self.public_key is None:
return False

Expand Down Expand Up @@ -279,10 +279,6 @@ def compute_node_id(self) -> NodeId | None:
Per EIP-778 "v4" identity scheme: keccak256(uncompressed_pubkey).
The hash is computed over the 64-byte x||y coordinates.
"""
from Crypto.Hash import keccak
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec

if self.public_key is None:
return None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from dataclasses import dataclass

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec

Expand Down Expand Up @@ -142,8 +143,6 @@ def verify_signature(
Returns:
True if signature is valid, False otherwise.
"""
from cryptography.exceptions import InvalidSignature

public_key = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256K1(),
public_key_bytes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

from aioquic.asyncio import QuicConnectionProtocol
from aioquic.asyncio import connect as quic_connect
Expand All @@ -40,13 +39,11 @@
StreamReset,
)

from ..identity import IdentityKeypair
from ..multistream import negotiate_lazy_client
from ..peer_id import PeerId
from .tls import generate_libp2p_certificate

if TYPE_CHECKING:
from ..identity import IdentityKeypair


class QuicTransportError(Exception):
"""Raised when QUIC connection operations fail."""
Expand Down Expand Up @@ -528,8 +525,6 @@ async def connect(self, multiaddr: str) -> QuicConnection:
else:
# Generate a random peer ID for now.
# This is NOT correct for production but allows testing.
from ..identity import IdentityKeypair

temp_key = IdentityKeypair.generate()
peer_id = temp_key.to_peer_id()

Expand Down Expand Up @@ -590,8 +585,6 @@ async def listen(
# Callback to set up connection when handshake completes.
# Captures this manager's state (self, on_connection, host, port).
def handle_handshake(protocol_instance: LibP2PQuicProtocol) -> None:
from ..identity import IdentityKeypair

temp_key = IdentityKeypair.generate()
remote_peer_id = temp_key.to_peer_id()

Expand Down
11 changes: 4 additions & 7 deletions src/lean_spec/subspecs/networking/transport/quic/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from __future__ import annotations

import hashlib
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -105,8 +106,6 @@ def generate_libp2p_certificate(
#
# SKI is the SHA-256 hash of the TLS public key, truncated to 20 bytes.
# This matches webpki's expected format for self-signed certificates.
import hashlib

ski_digest = hashlib.sha256(tls_public_bytes).digest()[:20]

cert = (
Expand Down Expand Up @@ -381,14 +380,12 @@ def _verify_identity_signature(
Raises:
ValueError: If signature is invalid.
"""
from cryptography.hazmat.primitives.asymmetric import ec as ec_module

# Reconstruct the secp256k1 public key.
#
# Compressed public key is 33 bytes (02/03 prefix + 32 byte X coordinate).
try:
public_key = ec_module.EllipticCurvePublicKey.from_encoded_point(
ec_module.SECP256K1(),
public_key = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256K1(),
public_key_bytes,
)
except Exception as e:
Expand All @@ -402,7 +399,7 @@ def _verify_identity_signature(
public_key.verify(
signature, # DER-encoded
to_verify,
ec_module.ECDSA(hashes.SHA256()),
ec.ECDSA(hashes.SHA256()),
)
except Exception as e:
raise ValueError(f"Signature verification failed: {e}") from e
Loading
Loading