From a8038e9e107370f03b6f67d3570068ec6155c53d Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Tue, 10 Feb 2026 19:21:01 +0100 Subject: [PATCH 1/3] core: better import classification --- CLAUDE.md | 17 +++ src/lean_spec/__main__.py | 22 ++- .../attestation/aggregation_bits.py | 12 +- .../networking/client/event_source.py | 3 +- .../networking/discovery/handshake.py | 13 +- .../subspecs/networking/discovery/keys.py | 10 +- .../subspecs/networking/discovery/service.py | 8 +- src/lean_spec/subspecs/networking/enr/enr.py | 20 ++- .../networking/transport/identity/keypair.py | 3 +- .../networking/transport/quic/connection.py | 9 +- .../subspecs/networking/transport/quic/tls.py | 11 +- src/lean_spec/subspecs/node/node.py | 7 +- .../subspecs/sync/checkpoint_sync.py | 12 +- src/lean_spec/subspecs/sync/service.py | 3 +- tests/api/conftest.py | 11 +- tests/interop/helpers/assertions.py | 4 +- tests/interop/helpers/node_runner.py | 7 +- tests/lean_spec/helpers/builders.py | 49 ++----- tests/lean_spec/snappy/test_snappy.py | 5 +- tests/lean_spec/subspecs/api/test_server.py | 6 +- .../lean_spec/subspecs/forkchoice/conftest.py | 6 +- .../forkchoice/test_attestation_target.py | 4 +- .../forkchoice/test_time_management.py | 18 +-- .../subspecs/koalabear/test_field.py | 17 +-- .../subspecs/metrics/test_registry.py | 8 +- .../networking/discovery/test_crypto.py | 3 +- .../networking/discovery/test_handshake.py | 48 ++----- .../networking/discovery/test_integration.py | 12 +- .../networking/discovery/test_keys.py | 3 +- .../networking/discovery/test_packet.py | 16 +-- .../networking/discovery/test_routing.py | 4 +- .../networking/discovery/test_service.py | 2 - .../networking/discovery/test_transport.py | 12 +- .../networking/discovery/test_vectors.py | 97 ++++--------- .../subspecs/networking/enr/test_enr.py | 93 ++---------- .../networking/gossipsub/test_gossipsub.py | 135 +++++------------- .../networking/reqresp/test_handler.py | 5 +- .../subspecs/networking/test_peer.py | 29 ++-- .../subspecs/networking/test_reqresp.py | 41 +----- .../transport/multistream/test_negotiation.py | 5 +- tests/lean_spec/subspecs/sync/test_service.py | 3 +- .../subspecs/validator/test_service.py | 10 +- .../subspecs/xmss/test_ssz_serialization.py | 4 +- .../subspecs/xmss/test_strict_types.py | 3 +- tests/lean_spec/test_cli.py | 28 ++-- tests/lean_spec/types/test_uint.py | 3 +- 46 files changed, 224 insertions(+), 617 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7cb7bc7b..3fbabc71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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: diff --git a/src/lean_spec/__main__.py b/src/lean_spec/__main__.py index 2d4341d0..5f524c76 100644 --- a/src/lean_spec/__main__.py +++ b/src/lean_spec/__main__.py @@ -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 @@ -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 @@ -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). @@ -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) @@ -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) @@ -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) diff --git a/src/lean_spec/subspecs/containers/attestation/aggregation_bits.py b/src/lean_spec/subspecs/containers/attestation/aggregation_bits.py index 24116277..3b12ea31 100644 --- a/src/lean_spec/subspecs/containers/attestation/aggregation_bits.py +++ b/src/lean_spec/subspecs/containers/attestation/aggregation_bits.py @@ -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): """ @@ -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 @@ -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: diff --git a/src/lean_spec/subspecs/networking/client/event_source.py b/src/lean_spec/subspecs/networking/client/event_source.py index a4973742..2f5a3e3c 100644 --- a/src/lean_spec/subspecs/networking/client/event_source.py +++ b/src/lean_spec/subspecs/networking/client/event_source.py @@ -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, @@ -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) diff --git a/src/lean_spec/subspecs/networking/discovery/handshake.py b/src/lean_spec/subspecs/networking/discovery/handshake.py index b57f3cd8..684f04bc 100644 --- a/src/lean_spec/subspecs/networking/discovery/handshake.py +++ b/src/lean_spec/subspecs/networking/discovery/handshake.py @@ -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 ( @@ -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, @@ -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.""" @@ -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) @@ -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. # diff --git a/src/lean_spec/subspecs/networking/discovery/keys.py b/src/lean_spec/subspecs/networking/discovery/keys.py index ea1562a8..37e7796c 100644 --- a/src/lean_spec/subspecs/networking/discovery/keys.py +++ b/src/lean_spec/subspecs/networking/discovery/keys.py @@ -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.""" @@ -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) @@ -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) diff --git a/src/lean_spec/subspecs/networking/discovery/service.py b/src/lean_spec/subspecs/networking/discovery/service.py index 3d60e7c3..9fa0dbd1 100644 --- a/src/lean_spec/subspecs/networking/discovery/service.py +++ b/src/lean_spec/subspecs/networking/discovery/service.py @@ -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 @@ -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 @@ -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) diff --git a/src/lean_spec/subspecs/networking/enr/enr.py b/src/lean_spec/subspecs/networking/enr/enr.py index f81c077e..063078a0 100644 --- a/src/lean_spec/subspecs/networking/enr/enr.py +++ b/src/lean_spec/subspecs/networking/enr/enr.py @@ -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, @@ -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 @@ -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 diff --git a/src/lean_spec/subspecs/networking/transport/identity/keypair.py b/src/lean_spec/subspecs/networking/transport/identity/keypair.py index d2f4f70d..7b23c3b1 100644 --- a/src/lean_spec/subspecs/networking/transport/identity/keypair.py +++ b/src/lean_spec/subspecs/networking/transport/identity/keypair.py @@ -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 @@ -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, diff --git a/src/lean_spec/subspecs/networking/transport/quic/connection.py b/src/lean_spec/subspecs/networking/transport/quic/connection.py index b8f86475..9494a7ae 100644 --- a/src/lean_spec/subspecs/networking/transport/quic/connection.py +++ b/src/lean_spec/subspecs/networking/transport/quic/connection.py @@ -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 @@ -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.""" @@ -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() @@ -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() diff --git a/src/lean_spec/subspecs/networking/transport/quic/tls.py b/src/lean_spec/subspecs/networking/transport/quic/tls.py index 39d14238..7a1b8827 100644 --- a/src/lean_spec/subspecs/networking/transport/quic/tls.py +++ b/src/lean_spec/subspecs/networking/transport/quic/tls.py @@ -22,6 +22,7 @@ from __future__ import annotations +import hashlib from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING @@ -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 = ( @@ -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: @@ -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 diff --git a/src/lean_spec/subspecs/node/node.py b/src/lean_spec/subspecs/node/node.py index eb245c1f..c00c9695 100644 --- a/src/lean_spec/subspecs/node/node.py +++ b/src/lean_spec/subspecs/node/node.py @@ -16,7 +16,6 @@ from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING from lean_spec.subspecs.api import ApiServer, ApiServerConfig from lean_spec.subspecs.chain import SlotClock @@ -31,13 +30,11 @@ from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.networking import NetworkEventSource, NetworkService from lean_spec.subspecs.ssz.hash import hash_tree_root +from lean_spec.subspecs.storage import Database, SQLiteDatabase from lean_spec.subspecs.sync import BlockCache, NetworkRequester, PeerManager, SyncService from lean_spec.subspecs.validator import ValidatorRegistry, ValidatorService from lean_spec.types import Bytes32, Uint64 -if TYPE_CHECKING: - from lean_spec.subspecs.storage import Database - @dataclass(frozen=True, slots=True) class NodeConfig: @@ -292,8 +289,6 @@ def _create_database(path: Path | str) -> Database: Returns: Database instance ready for use. """ - from lean_spec.subspecs.storage import SQLiteDatabase - # SQLite handles its own caching at the filesystem level. return SQLiteDatabase(path) diff --git a/src/lean_spec/subspecs/sync/checkpoint_sync.py b/src/lean_spec/subspecs/sync/checkpoint_sync.py index 32f323b8..9e88558c 100644 --- a/src/lean_spec/subspecs/sync/checkpoint_sync.py +++ b/src/lean_spec/subspecs/sync/checkpoint_sync.py @@ -19,12 +19,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Any +from typing import Any import httpx -if TYPE_CHECKING: - from lean_spec.subspecs.containers import State +from lean_spec.subspecs.chain.config import VALIDATOR_REGISTRY_LIMIT +from lean_spec.subspecs.containers import Slot, State +from lean_spec.subspecs.ssz.hash import hash_tree_root logger = logging.getLogger(__name__) @@ -125,11 +126,6 @@ async def verify_checkpoint_state(state: "State") -> bool: Returns: True if valid, False otherwise. """ - # Lazy imports to avoid circular dependency with containers - from lean_spec.subspecs.chain.config import VALIDATOR_REGISTRY_LIMIT - from lean_spec.subspecs.containers import Slot - from lean_spec.subspecs.ssz.hash import hash_tree_root - try: # Sanity check: slot must be non-negative. if state.slot < Slot(0): diff --git a/src/lean_spec/subspecs/sync/service.py b/src/lean_spec/subspecs/sync/service.py index e210d16f..fd6889c1 100644 --- a/src/lean_spec/subspecs/sync/service.py +++ b/src/lean_spec/subspecs/sync/service.py @@ -52,6 +52,7 @@ from lean_spec.subspecs.forkchoice.store import Store from lean_spec.subspecs.networking.reqresp.message import Status from lean_spec.subspecs.networking.transport.peer_id import PeerId +from lean_spec.subspecs.node.helpers import is_aggregator from lean_spec.subspecs.ssz.hash import hash_tree_root from .backfill_sync import BackfillSync, NetworkRequester @@ -425,8 +426,6 @@ async def on_gossip_attestation( if not self._state.accepts_gossip: return - from lean_spec.subspecs.node.helpers import is_aggregator - # Check if we are an aggregator is_aggregator_role = is_aggregator( self.store.validator_id, diff --git a/tests/api/conftest.py b/tests/api/conftest.py index d0dba590..41c000cb 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -3,13 +3,13 @@ import asyncio import threading import time -from typing import TYPE_CHECKING, Generator +from typing import Generator import httpx import pytest -if TYPE_CHECKING: - from lean_spec.subspecs.api import ApiServer +from lean_spec.subspecs.api import ApiServer, ApiServerConfig +from tests.lean_spec.helpers import make_store # Default port for auto-started local server DEFAULT_PORT = 15099 @@ -45,11 +45,8 @@ def run(self) -> None: if self.loop: self.loop.close() - def _create_server(self) -> "ApiServer": + def _create_server(self) -> ApiServer: """Create the API server with a test store.""" - from lean_spec.subspecs.api import ApiServer, ApiServerConfig - from tests.lean_spec.helpers import make_store - store = make_store(num_validators=3, validator_id=None, genesis_time=int(time.time())) config = ApiServerConfig(host="127.0.0.1", port=self.port) diff --git a/tests/interop/helpers/assertions.py b/tests/interop/helpers/assertions.py index 405f1465..07390516 100644 --- a/tests/interop/helpers/assertions.py +++ b/tests/interop/helpers/assertions.py @@ -9,12 +9,10 @@ import asyncio import logging import time -from typing import TYPE_CHECKING from lean_spec.types import Bytes32 -if TYPE_CHECKING: - from .node_runner import NodeCluster, TestNode +from .node_runner import NodeCluster, TestNode logger = logging.getLogger(__name__) diff --git a/tests/interop/helpers/node_runner.py b/tests/interop/helpers/node_runner.py index 691cc2af..71bc288a 100644 --- a/tests/interop/helpers/node_runner.py +++ b/tests/interop/helpers/node_runner.py @@ -10,7 +10,7 @@ import logging import time from dataclasses import dataclass, field -from typing import TYPE_CHECKING, cast +from typing import cast from lean_spec.subspecs.containers import Checkpoint, Validator from lean_spec.subspecs.containers.state import Validators @@ -23,14 +23,11 @@ from lean_spec.subspecs.node import Node, NodeConfig from lean_spec.subspecs.validator import ValidatorRegistry from lean_spec.subspecs.validator.registry import ValidatorEntry -from lean_spec.subspecs.xmss import TARGET_SIGNATURE_SCHEME +from lean_spec.subspecs.xmss import TARGET_SIGNATURE_SCHEME, SecretKey from lean_spec.types import Bytes32, Bytes52, Uint64 from .port_allocator import PortAllocator -if TYPE_CHECKING: - from lean_spec.subspecs.xmss import SecretKey - logger = logging.getLogger(__name__) diff --git a/tests/lean_spec/helpers/builders.py b/tests/lean_spec/helpers/builders.py index 9c7e2f56..ae557bfc 100644 --- a/tests/lean_spec/helpers/builders.py +++ b/tests/lean_spec/helpers/builders.py @@ -7,8 +7,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import NamedTuple, cast +from consensus_testing.keys import XmssKeyManager, get_shared_key_manager + +from lean_spec.subspecs.chain.clock import SlotClock +from lean_spec.subspecs.chain.config import SECONDS_PER_SLOT from lean_spec.subspecs.containers import ( Attestation, AttestationData, @@ -27,8 +31,17 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators from lean_spec.subspecs.containers.validator import ValidatorIndex +from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.koalabear import Fp +from lean_spec.subspecs.networking import PeerId +from lean_spec.subspecs.networking.peer.info import PeerInfo +from lean_spec.subspecs.networking.reqresp.message import Status +from lean_spec.subspecs.networking.types import ConnectionState from lean_spec.subspecs.ssz.hash import hash_tree_root +from lean_spec.subspecs.sync.block_cache import BlockCache +from lean_spec.subspecs.sync.peer_manager import PeerManager +from lean_spec.subspecs.sync.service import SyncService +from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof, SignatureKey from lean_spec.subspecs.xmss.constants import PROD_CONFIG from lean_spec.subspecs.xmss.containers import PublicKey, Signature from lean_spec.subspecs.xmss.types import ( @@ -40,14 +53,7 @@ ) from lean_spec.types import Bytes32, Bytes52, Uint64 -if TYPE_CHECKING: - from consensus_testing.keys import XmssKeyManager - - from lean_spec.subspecs.forkchoice import Store - from lean_spec.subspecs.networking import PeerId - from lean_spec.subspecs.networking.reqresp.message import Status - from lean_spec.subspecs.sync.service import SyncService - from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof +from .mocks import MockForkchoiceStore, MockNetworkRequester def make_bytes32(seed: int) -> Bytes32: @@ -308,8 +314,6 @@ def make_signed_attestation( def make_test_status() -> Status: """Create a valid Status message for testing.""" - from lean_spec.subspecs.networking.reqresp.message import Status - return Status( finalized=Checkpoint(root=Bytes32(b"\x01" * 32), slot=Slot(100)), head=Checkpoint(root=Bytes32(b"\x02" * 32), slot=Slot(200)), @@ -359,8 +363,6 @@ def make_genesis_data( validator_id: ValidatorIndex | None = _DEFAULT_VALIDATOR_ID, ) -> GenesisData: """Create a forkchoice store with genesis state and block, returning all three.""" - from lean_spec.subspecs.forkchoice import Store - if key_manager is not None: validators = make_validators_from_key_manager(key_manager, num_validators) else: @@ -410,8 +412,6 @@ def make_store_with_gossip_signatures( attestation_slot: Slot = _DEFAULT_ATTESTATION_SLOT, ) -> tuple[Store, AttestationData]: """Create a store pre-populated with gossip signatures for testing aggregation.""" - from lean_spec.subspecs.xmss.aggregation import SignatureKey - store, attestation_data = make_store_with_attestation_data( key_manager, num_validators, @@ -453,8 +453,6 @@ def make_keyed_genesis_state( ) -> State: """Create a genesis state with real XMSS keys from the shared key manager.""" if key_manager is None: - from consensus_testing.keys import get_shared_key_manager - key_manager = get_shared_key_manager() validators = make_validators_from_key_manager(key_manager, num_validators) return make_genesis_state(validators=validators) @@ -466,8 +464,6 @@ def make_aggregated_proof( attestation_data: AttestationData, ) -> AggregatedSignatureProof: """Create a valid aggregated signature proof for the given participants.""" - from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof - data_root = attestation_data.data_root_bytes() return AggregatedSignatureProof.aggregate( participants=AggregationBits.from_validator_indices(participants), @@ -490,8 +486,6 @@ def make_signed_block_from_store( Returns the updated store (with time advanced) and the signed block. """ - from lean_spec.subspecs.chain.config import SECONDS_PER_SLOT - _, block, _ = store.produce_block_with_signatures(slot, proposer_index) block_root = hash_tree_root(block) parent_state = store.states[block.parent_root] @@ -533,19 +527,6 @@ def make_signed_block_from_store( def create_mock_sync_service(peer_id: PeerId) -> SyncService: """Create a SyncService with mock dependencies for integration testing.""" - from typing import cast - - from lean_spec.subspecs.chain.clock import SlotClock - from lean_spec.subspecs.forkchoice import Store - from lean_spec.subspecs.networking.peer.info import PeerInfo - from lean_spec.subspecs.networking.types import ConnectionState - from lean_spec.subspecs.sync.block_cache import BlockCache - from lean_spec.subspecs.sync.peer_manager import PeerManager - from lean_spec.subspecs.sync.service import SyncService - from lean_spec.types import Uint64 - - from .mocks import MockForkchoiceStore, MockNetworkRequester - mock_store = MockForkchoiceStore(head_slot=0) peer_manager = PeerManager() peer_manager.add_peer(PeerInfo(peer_id=peer_id, state=ConnectionState.CONNECTED)) diff --git a/tests/lean_spec/snappy/test_snappy.py b/tests/lean_spec/snappy/test_snappy.py index 54f2033c..db3e2832 100644 --- a/tests/lean_spec/snappy/test_snappy.py +++ b/tests/lean_spec/snappy/test_snappy.py @@ -2,6 +2,7 @@ from __future__ import annotations +import random from pathlib import Path from typing import Iterator @@ -238,8 +239,6 @@ def test_basic_self_patterns(self) -> None: def test_exhaustive_self_patterns(self) -> None: """Exhaustive test of self-extending patterns with various sizes.""" - import random - random.seed(42) # Deterministic for reproducibility for pattern_size in range(1, 19): @@ -269,8 +268,6 @@ class TestMaxBlowup: def test_max_blowup(self) -> None: """Test worst-case compression expansion (lots of four-byte copies).""" - import random - random.seed(42) # Build input with random bytes diff --git a/tests/lean_spec/subspecs/api/test_server.py b/tests/lean_spec/subspecs/api/test_server.py index 2d51a48f..860ec606 100644 --- a/tests/lean_spec/subspecs/api/test_server.py +++ b/tests/lean_spec/subspecs/api/test_server.py @@ -13,6 +13,8 @@ import asyncio +import httpx + from lean_spec.subspecs.api import ApiServer, ApiServerConfig from lean_spec.subspecs.forkchoice import Store @@ -65,8 +67,6 @@ class TestFinalizedStateEndpoint: async def test_returns_503_when_store_not_initialized(self) -> None: """Endpoint returns 503 Service Unavailable when store is not set.""" - import httpx - config = ApiServerConfig(port=15054) server = ApiServer(config=config) @@ -88,8 +88,6 @@ class TestJustifiedCheckpointEndpoint: async def test_returns_503_when_store_not_initialized(self) -> None: """Endpoint returns 503 Service Unavailable when store is not set.""" - import httpx - config = ApiServerConfig(port=15057) server = ApiServer(config=config) diff --git a/tests/lean_spec/subspecs/forkchoice/conftest.py b/tests/lean_spec/subspecs/forkchoice/conftest.py index 8fc0ba25..9b11caf4 100644 --- a/tests/lean_spec/subspecs/forkchoice/conftest.py +++ b/tests/lean_spec/subspecs/forkchoice/conftest.py @@ -6,7 +6,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Type +from typing import Type import pytest @@ -22,13 +22,11 @@ JustifiedSlots, ) from lean_spec.subspecs.containers.validator import ValidatorIndex +from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Uint64 from tests.lean_spec.helpers import TEST_VALIDATOR_ID, make_store -if TYPE_CHECKING: - from lean_spec.subspecs.forkchoice import Store - class MockState(State): """Mock state with configurable latest_justified checkpoint.""" diff --git a/tests/lean_spec/subspecs/forkchoice/test_attestation_target.py b/tests/lean_spec/subspecs/forkchoice/test_attestation_target.py index 43f72e05..2cbc306f 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_attestation_target.py +++ b/tests/lean_spec/subspecs/forkchoice/test_attestation_target.py @@ -18,6 +18,7 @@ ) from lean_spec.subspecs.containers.attestation import SignedAttestation from lean_spec.subspecs.containers.block import BlockSignatures +from lean_spec.subspecs.containers.block.types import AttestationSignatures from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.validator import ValidatorIndex from lean_spec.subspecs.forkchoice import Store @@ -574,9 +575,6 @@ def test_attestation_target_after_on_block( proposer_attestation.data, ) - # Create signed block for on_block processing - from lean_spec.subspecs.containers.block.types import AttestationSignatures - signed_block = SignedBlockWithAttestation( message=BlockWithAttestation( block=block, diff --git a/tests/lean_spec/subspecs/forkchoice/test_time_management.py b/tests/lean_spec/subspecs/forkchoice/test_time_management.py index 228f1b82..36df3b51 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_time_management.py +++ b/tests/lean_spec/subspecs/forkchoice/test_time_management.py @@ -3,7 +3,12 @@ from hypothesis import given, settings from hypothesis import strategies as st -from lean_spec.subspecs.chain.config import INTERVALS_PER_SLOT +from lean_spec.subspecs.chain.config import ( + INTERVALS_PER_SLOT, + MILLISECONDS_PER_INTERVAL, + MILLISECONDS_PER_SLOT, + SECONDS_PER_SLOT, +) from lean_spec.subspecs.containers import Block, State from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators @@ -127,8 +132,6 @@ def test_tick_interval_sequence(self, sample_store: Store) -> None: def test_tick_interval_actions_by_phase(self, sample_store: Store) -> None: """Test different actions performed based on interval phase.""" - from lean_spec.subspecs.chain.config import INTERVALS_PER_SLOT - # Reset store to known state initial_time = Uint64(0) object.__setattr__(sample_store, "time", initial_time) @@ -234,13 +237,6 @@ class TestTimeConstants: def test_time_constants_consistency(self) -> None: """Test that time constants are consistent with each other.""" - from lean_spec.subspecs.chain.config import ( - INTERVALS_PER_SLOT, - MILLISECONDS_PER_INTERVAL, - MILLISECONDS_PER_SLOT, - SECONDS_PER_SLOT, - ) - # MILLISECONDS_PER_SLOT should equal INTERVALS_PER_SLOT * MILLISECONDS_PER_INTERVAL expected_milliseconds_per_slot = INTERVALS_PER_SLOT * MILLISECONDS_PER_INTERVAL assert MILLISECONDS_PER_SLOT == expected_milliseconds_per_slot @@ -256,8 +252,6 @@ def test_time_constants_consistency(self) -> None: def test_interval_slot_relationship(self) -> None: """Test the relationship between intervals and slots.""" - from lean_spec.subspecs.chain.config import INTERVALS_PER_SLOT - # Should have multiple intervals per slot assert INTERVALS_PER_SLOT >= Uint64(2) # At least 2 intervals per slot diff --git a/tests/lean_spec/subspecs/koalabear/test_field.py b/tests/lean_spec/subspecs/koalabear/test_field.py index c5a86eeb..83dc40f8 100644 --- a/tests/lean_spec/subspecs/koalabear/test_field.py +++ b/tests/lean_spec/subspecs/koalabear/test_field.py @@ -2,6 +2,9 @@ Tests for the KoalaBear prime field Fp. """ +import io +import random + import pytest from lean_spec.subspecs.koalabear.field import ( @@ -157,8 +160,6 @@ def test_deserialize_list() -> None: def test_serialize_list_roundtrip_property() -> None: """Property test: serialization round-trip should preserve values.""" - import random - random.seed(42) for _ in range(100): @@ -185,8 +186,6 @@ def test_ssz_type_properties() -> None: def test_ssz_serialize() -> None: """Test SSZ serialization using the serialize method.""" - import io - fp = Fp(value=42) # Test serialize to stream @@ -198,8 +197,6 @@ def test_ssz_serialize() -> None: def test_ssz_deserialize() -> None: """Test SSZ deserialization using the deserialize method.""" - import io - # Test successful deserialization data = b"\x2a\x00\x00\x00" # 42 in LE stream = io.BytesIO(data) @@ -209,8 +206,6 @@ def test_ssz_deserialize() -> None: def test_ssz_deserialize_wrong_scope() -> None: """Test deserialize error when scope doesn't match P_BYTES.""" - import io - data = b"\x2a\x00\x00\x00" stream = io.BytesIO(data) with pytest.raises(ValueError, match="Expected 4 bytes for Fp, got 3"): @@ -219,8 +214,6 @@ def test_ssz_deserialize_wrong_scope() -> None: def test_ssz_deserialize_short_data() -> None: """Test deserialize error when stream has insufficient data.""" - import io - stream = io.BytesIO(b"\x01\x02\x03") # Only 3 bytes with pytest.raises(ValueError, match="Expected 4 bytes for Fp, got 3"): Fp.deserialize(stream, 4) @@ -228,8 +221,6 @@ def test_ssz_deserialize_short_data() -> None: def test_ssz_deserialize_exceeds_modulus() -> None: """Test deserialize error when value exceeds field modulus.""" - import io - # P = 2^31 - 2^24 + 1 = 2130706433 # Encode a value >= P (use P itself) invalid_data = P.to_bytes(4, byteorder="little") @@ -261,8 +252,6 @@ def test_ssz_encode_decode_bytes() -> None: def test_ssz_roundtrip() -> None: """Comprehensive SSZ roundtrip test with many values.""" - import random - random.seed(12345) for _ in range(100): diff --git a/tests/lean_spec/subspecs/metrics/test_registry.py b/tests/lean_spec/subspecs/metrics/test_registry.py index fbea11e8..ea151354 100644 --- a/tests/lean_spec/subspecs/metrics/test_registry.py +++ b/tests/lean_spec/subspecs/metrics/test_registry.py @@ -2,6 +2,10 @@ from __future__ import annotations +import time + +from prometheus_client import REGISTRY as DEFAULT_REGISTRY + from lean_spec.subspecs.metrics import ( REGISTRY, attestations_produced, @@ -106,8 +110,6 @@ class TestRegistryIsolation: def test_registry_is_dedicated(self) -> None: """Our registry is separate from default prometheus registry.""" - from prometheus_client import REGISTRY as DEFAULT_REGISTRY - assert REGISTRY is not DEFAULT_REGISTRY def test_metrics_registered_to_custom_registry(self) -> None: @@ -124,8 +126,6 @@ class TestHistogramTiming: def test_time_context_manager_records_duration(self) -> None: """Histogram time() context manager records duration.""" - import time - # Get initial values from samples initial_samples = list(block_processing_time.collect())[0].samples initial_count = next(s.value for s in initial_samples if s.name.endswith("_count")) diff --git a/tests/lean_spec/subspecs/networking/discovery/test_crypto.py b/tests/lean_spec/subspecs/networking/discovery/test_crypto.py index 672f99fd..58b0f7f6 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_crypto.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_crypto.py @@ -1,6 +1,7 @@ """Tests for Discovery v5 cryptographic primitives.""" import pytest +from cryptography.exceptions import InvalidTag from lean_spec.subspecs.networking.discovery.crypto import ( aes_ctr_decrypt, @@ -93,8 +94,6 @@ def test_ciphertext_includes_auth_tag(self): def test_wrong_aad_fails_decryption(self): """Test that wrong AAD causes authentication failure.""" - from cryptography.exceptions import InvalidTag - key = Bytes16.zero() nonce = Bytes12.zero() plaintext = b"secret" diff --git a/tests/lean_spec/subspecs/networking/discovery/test_handshake.py b/tests/lean_spec/subspecs/networking/discovery/test_handshake.py index 81c2cc7c..ef2762f9 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_handshake.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_handshake.py @@ -4,17 +4,27 @@ import pytest -from lean_spec.subspecs.networking.discovery.crypto import generate_secp256k1_keypair +from lean_spec.subspecs.networking.discovery.crypto import ( + generate_secp256k1_keypair, + sign_id_nonce, +) from lean_spec.subspecs.networking.discovery.handshake import ( + HandshakeError, HandshakeManager, + HandshakeResult, HandshakeState, PendingHandshake, ) from lean_spec.subspecs.networking.discovery.keys import compute_node_id from lean_spec.subspecs.networking.discovery.packet import ( + HandshakeAuthdata, + decode_handshake_authdata, decode_whoareyou_authdata, + encode_handshake_authdata, ) from lean_spec.subspecs.networking.discovery.session import SessionCache +from lean_spec.subspecs.networking.enr import ENR +from lean_spec.types import Bytes32, Bytes33, Bytes64, Uint64 from tests.lean_spec.subspecs.networking.discovery.conftest import NODE_B_PUBKEY @@ -367,13 +377,9 @@ def manager(self, local_keypair, session_cache): def test_handle_handshake_requires_pending_state(self, manager, remote_keypair): """Handshake fails if no pending state exists for the remote.""" - from lean_spec.subspecs.networking.discovery.handshake import HandshakeError - remote_priv, remote_pub, remote_node_id = remote_keypair # Create fake handshake authdata. - from lean_spec.subspecs.networking.discovery.packet import HandshakeAuthdata - fake_authdata = HandshakeAuthdata( src_id=bytes(remote_node_id), sig_size=64, @@ -389,15 +395,11 @@ def test_handle_handshake_requires_pending_state(self, manager, remote_keypair): def test_handle_handshake_requires_sent_whoareyou_state(self, manager, remote_keypair): """Handshake fails if not in SENT_WHOAREYOU state.""" - from lean_spec.subspecs.networking.discovery.handshake import HandshakeError - remote_priv, remote_pub, remote_node_id = remote_keypair # Start handshake (puts in SENT_ORDINARY state). manager.start_handshake(bytes(remote_node_id)) - from lean_spec.subspecs.networking.discovery.packet import HandshakeAuthdata - fake_authdata = HandshakeAuthdata( src_id=bytes(remote_node_id), sig_size=64, @@ -413,8 +415,6 @@ def test_handle_handshake_requires_sent_whoareyou_state(self, manager, remote_ke def test_handle_handshake_rejects_src_id_mismatch(self, manager, remote_keypair): """Handshake fails if src_id doesn't match expected remote.""" - from lean_spec.subspecs.networking.discovery.handshake import HandshakeError - remote_priv, remote_pub, remote_node_id = remote_keypair # Set up WHOAREYOU state. @@ -426,8 +426,6 @@ def test_handle_handshake_rejects_src_id_mismatch(self, manager, remote_keypair) ) # Create authdata with different src_id. - from lean_spec.subspecs.networking.discovery.packet import HandshakeAuthdata - wrong_src_id = bytes([0xFF] * 32) fake_authdata = HandshakeAuthdata( src_id=wrong_src_id, @@ -448,8 +446,6 @@ def test_handle_handshake_requires_enr_when_seq_zero(self, manager, remote_keypa When we don't know the remote's ENR (signaled by enr_seq=0 in WHOAREYOU), the remote MUST include their ENR in the HANDSHAKE response. """ - from lean_spec.subspecs.networking.discovery.handshake import HandshakeError - remote_priv, remote_pub, remote_node_id = remote_keypair # Set up WHOAREYOU with enr_seq=0 (unknown). @@ -460,8 +456,6 @@ def test_handle_handshake_requires_enr_when_seq_zero(self, manager, remote_keypa bytes(16), ) - from lean_spec.subspecs.networking.discovery.packet import HandshakeAuthdata - # Create authdata without ENR record. fake_authdata = HandshakeAuthdata( src_id=bytes(remote_node_id), @@ -483,15 +477,6 @@ def test_successful_handshake_with_signature_verification( Exercises the complete WHOAREYOU -> HANDSHAKE -> session flow. """ - from lean_spec.subspecs.networking.discovery.crypto import sign_id_nonce - from lean_spec.subspecs.networking.discovery.handshake import HandshakeResult - from lean_spec.subspecs.networking.discovery.packet import ( - decode_handshake_authdata, - encode_handshake_authdata, - ) - from lean_spec.subspecs.networking.enr.enr import ENR - from lean_spec.types import Bytes32, Bytes33, Bytes64, Uint64 - remote_priv, remote_pub, remote_node_id = remote_keypair # Node A (manager) creates WHOAREYOU for remote. @@ -541,14 +526,6 @@ def test_handle_handshake_rejects_invalid_signature( self, manager, remote_keypair, session_cache ): """Handshake fails when signature is invalid.""" - from lean_spec.subspecs.networking.discovery.handshake import HandshakeError - from lean_spec.subspecs.networking.discovery.packet import ( - decode_handshake_authdata, - encode_handshake_authdata, - ) - from lean_spec.subspecs.networking.enr.enr import ENR - from lean_spec.types import Bytes64, Uint64 - remote_priv, remote_pub, remote_node_id = remote_keypair # Set up WHOAREYOU state. @@ -714,9 +691,6 @@ def manager(self, local_keypair): def test_register_enr_stores_in_cache(self, manager): """Registered ENRs are retrievable from cache.""" - from lean_spec.subspecs.networking.enr import ENR - from lean_spec.types import Bytes64, Uint64 - remote_node_id = bytes(compute_node_id(NODE_B_PUBKEY)) enr = ENR( diff --git a/tests/lean_spec/subspecs/networking/discovery/test_integration.py b/tests/lean_spec/subspecs/networking/discovery/test_integration.py index e8107a66..6836ad07 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_integration.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_integration.py @@ -6,6 +6,8 @@ from __future__ import annotations +import time + import pytest from lean_spec.subspecs.networking.discovery.codec import ( @@ -19,6 +21,7 @@ from lean_spec.subspecs.networking.discovery.handshake import HandshakeManager from lean_spec.subspecs.networking.discovery.keys import compute_node_id, derive_keys_from_pubkey from lean_spec.subspecs.networking.discovery.messages import ( + Distance, FindNode, MessageType, Nodes, @@ -45,6 +48,7 @@ from lean_spec.subspecs.networking.enr import ENR from lean_spec.subspecs.networking.types import NodeId, SeqNumber from lean_spec.types import Bytes12, Bytes16, Bytes32, Bytes64, Uint64 +from lean_spec.types.uint import Uint8 @pytest.fixture @@ -99,8 +103,6 @@ def test_pong_roundtrip(self): def test_findnode_roundtrip(self): """FINDNODE message encodes and decodes correctly.""" - from lean_spec.subspecs.networking.discovery.messages import Distance - original = FindNode( request_id=RequestId(data=b"\x01\x02\x03"), distances=[Distance(128), Distance(256)], @@ -121,8 +123,6 @@ def test_nodes_roundtrip(self): pairs={"id": b"v4"}, ) - from lean_spec.types.uint import Uint8 - original = Nodes( request_id=RequestId(data=b"\x01\x02\x03"), total=Uint8(1), @@ -217,8 +217,6 @@ class TestSessionEstablishment: def test_session_cache_operations(self, node_a_keys, node_b_keys): """Session cache stores and retrieves sessions.""" - import time - cache = SessionCache() now = time.time() @@ -244,8 +242,6 @@ def test_session_cache_operations(self, node_a_keys, node_b_keys): def test_session_cache_eviction(self, node_a_keys): """Session cache evicts old sessions when full.""" - import time - cache = SessionCache(max_sessions=3) # Add 4 sessions. diff --git a/tests/lean_spec/subspecs/networking/discovery/test_keys.py b/tests/lean_spec/subspecs/networking/discovery/test_keys.py index 85b266f4..b30bc151 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_keys.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_keys.py @@ -4,6 +4,7 @@ from lean_spec.subspecs.networking.discovery.crypto import ( generate_secp256k1_keypair, + pubkey_to_uncompressed, ) from lean_spec.subspecs.networking.discovery.keys import ( compute_node_id, @@ -161,7 +162,6 @@ def test_accepts_compressed_pubkey(self): def test_accepts_uncompressed_pubkey(self): """Test that uncompressed public key format is accepted.""" - from lean_spec.subspecs.networking.discovery.crypto import pubkey_to_uncompressed _, compressed = generate_secp256k1_keypair() uncompressed = pubkey_to_uncompressed(compressed) @@ -172,7 +172,6 @@ def test_accepts_uncompressed_pubkey(self): def test_compressed_and_uncompressed_produce_same_id(self): """Test that both formats produce the same node ID.""" - from lean_spec.subspecs.networking.discovery.crypto import pubkey_to_uncompressed _, compressed = generate_secp256k1_keypair() uncompressed = pubkey_to_uncompressed(compressed) diff --git a/tests/lean_spec/subspecs/networking/discovery/test_packet.py b/tests/lean_spec/subspecs/networking/discovery/test_packet.py index d4a1d004..735ef1ac 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_packet.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_packet.py @@ -2,6 +2,8 @@ import pytest +from lean_spec.subspecs.networking.discovery.config import MAX_PACKET_SIZE, MIN_PACKET_SIZE +from lean_spec.subspecs.networking.discovery.crypto import aes_ctr_encrypt from lean_spec.subspecs.networking.discovery.messages import PacketFlag from lean_spec.subspecs.networking.discovery.packet import ( HANDSHAKE_HEADER_SIZE, @@ -298,22 +300,16 @@ class TestPacketSizeLimits: def test_min_packet_size_constant(self): """MIN_PACKET_SIZE matches spec minimum.""" - from lean_spec.subspecs.networking.discovery.config import MIN_PACKET_SIZE - # masking-iv (16) + static-header (23) + min authdata (24 for WHOAREYOU) assert MIN_PACKET_SIZE == 63 def test_max_packet_size_constant(self): """MAX_PACKET_SIZE matches IPv6 MTU.""" - from lean_spec.subspecs.networking.discovery.config import MAX_PACKET_SIZE - # IPv6 minimum MTU = 1280 bytes assert MAX_PACKET_SIZE == 1280 def test_reject_undersized_packet(self): """Packets smaller than MIN_PACKET_SIZE are rejected.""" - from lean_spec.subspecs.networking.discovery.config import MIN_PACKET_SIZE - local_node_id = bytes(32) # Packet that's too small. @@ -324,8 +320,6 @@ def test_reject_undersized_packet(self): def test_minimum_valid_packet_structure(self): """Minimum valid packet has correct structure.""" - from lean_spec.subspecs.networking.discovery.config import MIN_PACKET_SIZE - # WHOAREYOU is the smallest packet type: # masking-iv (16) + static-header (23) + authdata (24) = 63 bytes expected_min = 16 + STATIC_HEADER_SIZE + WHOAREYOU_AUTHDATA_SIZE @@ -371,8 +365,6 @@ def test_truncated_static_header_rejected(self): def test_truncated_authdata_rejected(self): """Packet with incomplete authdata is rejected.""" - from lean_spec.subspecs.networking.discovery.crypto import aes_ctr_encrypt - local_node_id = bytes(32) masking_iv = bytes(16) @@ -398,8 +390,6 @@ class TestPacketProtocolValidation: def test_invalid_protocol_id_rejected(self): """Packet with wrong protocol ID is rejected.""" - from lean_spec.subspecs.networking.discovery.crypto import aes_ctr_encrypt - local_node_id = bytes(32) masking_iv = bytes(16) @@ -421,8 +411,6 @@ def test_invalid_protocol_id_rejected(self): def test_invalid_protocol_version_rejected(self): """Packet with unsupported protocol version is rejected.""" - from lean_spec.subspecs.networking.discovery.crypto import aes_ctr_encrypt - local_node_id = bytes(32) masking_iv = bytes(16) diff --git a/tests/lean_spec/subspecs/networking/discovery/test_routing.py b/tests/lean_spec/subspecs/networking/discovery/test_routing.py index 949f8786..68fd7588 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_routing.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_routing.py @@ -18,6 +18,7 @@ xor_distance, ) from lean_spec.subspecs.networking.enr import ENR +from lean_spec.subspecs.networking.enr.eth2 import FAR_FUTURE_EPOCH from lean_spec.subspecs.networking.types import NodeId, SeqNumber from lean_spec.types import Bytes64, Uint64 from lean_spec.types.byte_arrays import Bytes4 @@ -470,7 +471,6 @@ def test_fork_filter_rejects_without_eth2_data(self, local_node_id, remote_node_ def test_fork_filter_rejects_mismatched_fork(self, local_node_id, remote_node_id): """Node with different fork_digest is rejected.""" - from lean_spec.subspecs.networking.enr.eth2 import FAR_FUTURE_EPOCH local_fork = Bytes4(bytes.fromhex("12345678")) table = RoutingTable(local_id=local_node_id, local_fork_digest=local_fork) @@ -490,7 +490,6 @@ def test_fork_filter_rejects_mismatched_fork(self, local_node_id, remote_node_id def test_fork_filter_accepts_matching_fork(self, local_node_id, remote_node_id): """Node with matching fork_digest is accepted.""" - from lean_spec.subspecs.networking.enr.eth2 import FAR_FUTURE_EPOCH local_fork = Bytes4(bytes.fromhex("12345678")) table = RoutingTable(local_id=local_node_id, local_fork_digest=local_fork) @@ -513,7 +512,6 @@ def test_fork_filter_accepts_matching_fork(self, local_node_id, remote_node_id): def test_is_fork_compatible_method(self, local_node_id): """Verify is_fork_compatible for compatible, incompatible, and no-ENR entries.""" - from lean_spec.subspecs.networking.enr.eth2 import FAR_FUTURE_EPOCH local_fork = Bytes4(bytes.fromhex("12345678")) table = RoutingTable(local_id=local_node_id, local_fork_digest=local_fork) diff --git a/tests/lean_spec/subspecs/networking/discovery/test_service.py b/tests/lean_spec/subspecs/networking/discovery/test_service.py index 8dfcb07f..2fb46219 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_service.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_service.py @@ -458,8 +458,6 @@ def test_lookup_result_tracks_queries(self, local_enr, local_private_key): def test_lookup_result_contains_nodes(self, local_enr, local_private_key): """LookupResult contains found nodes.""" - from lean_spec.subspecs.networking.types import NodeId - nodes = [NodeEntry(node_id=NodeId(bytes([i]) + bytes(31))) for i in range(3)] result = LookupResult( diff --git a/tests/lean_spec/subspecs/networking/discovery/test_transport.py b/tests/lean_spec/subspecs/networking/discovery/test_transport.py index 1e406ee1..a7a991d6 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_transport.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_transport.py @@ -13,6 +13,7 @@ from lean_spec.subspecs.networking.discovery.config import DiscoveryConfig from lean_spec.subspecs.networking.discovery.messages import ( + Nodes, Ping, Pong, Port, @@ -21,10 +22,12 @@ from lean_spec.subspecs.networking.discovery.transport import ( DiscoveryProtocol, DiscoveryTransport, + PendingMultiRequest, PendingRequest, ) from lean_spec.subspecs.networking.enr import ENR from lean_spec.types import Bytes64, Uint64 +from lean_spec.types.uint import Uint8 class TestDiscoveryProtocol: @@ -395,7 +398,6 @@ class TestMultiPacketNodesCollection: def test_pending_multi_request_creation(self, local_node_id, local_private_key, local_enr): """PendingMultiRequest stores all required fields.""" - from lean_spec.subspecs.networking.discovery.transport import PendingMultiRequest loop = asyncio.new_event_loop() queue: asyncio.Queue = asyncio.Queue() @@ -419,7 +421,6 @@ def test_pending_multi_request_creation(self, local_node_id, local_private_key, def test_pending_multi_request_expected_total_tracking(self): """expected_total is set from first NODES response.""" - from lean_spec.subspecs.networking.discovery.transport import PendingMultiRequest loop = asyncio.new_event_loop() queue: asyncio.Queue = asyncio.Queue() @@ -453,7 +454,6 @@ def test_pending_multi_request_expected_total_tracking(self): def test_pending_multi_request_queue_usage(self): """Response queue collects multiple messages.""" - from lean_spec.subspecs.networking.discovery.transport import PendingMultiRequest loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -498,8 +498,6 @@ class TestNodesResponseAccumulation: def test_empty_nodes_response_handling(self): """NODES with total=0 indicates no results.""" - from lean_spec.subspecs.networking.discovery.messages import Nodes - from lean_spec.types.uint import Uint8 nodes = Nodes( request_id=RequestId(data=b"\x01"), @@ -512,8 +510,6 @@ def test_empty_nodes_response_handling(self): def test_single_nodes_response_collection(self): """Single NODES response with total=1.""" - from lean_spec.subspecs.networking.discovery.messages import Nodes - from lean_spec.types.uint import Uint8 nodes = Nodes( request_id=RequestId(data=b"\x01"), @@ -526,8 +522,6 @@ def test_single_nodes_response_collection(self): def test_multiple_nodes_responses_expected(self): """Multiple NODES messages share same request_id.""" - from lean_spec.subspecs.networking.discovery.messages import Nodes - from lean_spec.types.uint import Uint8 request_id = RequestId(data=b"\x01\x02\x03\x04") diff --git a/tests/lean_spec/subspecs/networking/discovery/test_vectors.py b/tests/lean_spec/subspecs/networking/discovery/test_vectors.py index 45d8ddfd..9e671e5c 100644 --- a/tests/lean_spec/subspecs/networking/discovery/test_vectors.py +++ b/tests/lean_spec/subspecs/networking/discovery/test_vectors.py @@ -9,6 +9,12 @@ from __future__ import annotations +import pytest +from cryptography.exceptions import InvalidTag +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + +from lean_spec.subspecs.networking.discovery.codec import encode_message from lean_spec.subspecs.networking.discovery.crypto import ( aes_gcm_decrypt, aes_gcm_encrypt, @@ -20,8 +26,21 @@ compute_node_id, derive_keys, ) -from lean_spec.subspecs.networking.discovery.messages import PacketFlag +from lean_spec.subspecs.networking.discovery.messages import ( + Distance, + FindNode, + MessageType, + Nodes, + PacketFlag, + Ping, + Pong, + Port, + RequestId, +) from lean_spec.subspecs.networking.discovery.packet import ( + HANDSHAKE_HEADER_SIZE, + STATIC_HEADER_SIZE, + WHOAREYOU_AUTHDATA_SIZE, decode_handshake_authdata, decode_message_authdata, decode_packet_header, @@ -32,7 +51,10 @@ encode_packet, encode_whoareyou_authdata, ) -from lean_spec.types import Bytes12, Bytes16, Bytes32, Bytes33, Bytes64 +from lean_spec.subspecs.networking.discovery.routing import log2_distance, xor_distance +from lean_spec.subspecs.networking.types import NodeId +from lean_spec.types import Bytes12, Bytes16, Bytes32, Bytes33, Bytes64, Uint64 +from lean_spec.types.uint import Uint8 from tests.lean_spec.helpers import make_challenge_data from tests.lean_spec.subspecs.networking.discovery.conftest import ( NODE_A_ID, @@ -79,9 +101,6 @@ def test_node_b_id_from_privkey(self): We derive the public key from the private key since the spec provides the private key for Node B. """ - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import ec - # Derive public key from private key. private_key = ec.derive_private_key( int.from_bytes(NODE_B_PRIVKEY, "big"), @@ -160,9 +179,6 @@ def test_id_nonce_signature(self): assert signature == expected_sig # Also verify the signature. - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import ec - private_key = ec.derive_private_key( int.from_bytes(SPEC_EPHEMERAL_KEY, "big"), ec.SECP256K1(), @@ -404,10 +420,6 @@ def test_official_ping_message_rlp_encoding(self): PING format: [request-id, enr-seq] Message type byte 0x01 prepended. """ - from lean_spec.subspecs.networking.discovery.codec import encode_message - from lean_spec.subspecs.networking.discovery.messages import MessageType, Ping, RequestId - from lean_spec.types import Uint64 - # PING with request ID [0x00, 0x00, 0x00, 0x01] and enr_seq = 1 ping = Ping( request_id=RequestId(data=b"\x00\x00\x00\x01"), @@ -429,15 +441,6 @@ def test_official_pong_message_rlp_encoding(self): PONG format: [request-id, enr-seq, recipient-ip, recipient-port] Message type byte 0x02 prepended. """ - from lean_spec.subspecs.networking.discovery.codec import encode_message - from lean_spec.subspecs.networking.discovery.messages import ( - MessageType, - Pong, - Port, - RequestId, - ) - from lean_spec.types import Uint64 - pong = Pong( request_id=RequestId(data=b"\x00\x00\x00\x01"), enr_seq=Uint64(1), @@ -457,14 +460,6 @@ def test_official_findnode_message_rlp_encoding(self): FINDNODE format: [request-id, [distances...]] Message type byte 0x03 prepended. """ - from lean_spec.subspecs.networking.discovery.codec import encode_message - from lean_spec.subspecs.networking.discovery.messages import ( - Distance, - FindNode, - MessageType, - RequestId, - ) - findnode = FindNode( request_id=RequestId(data=b"\x00\x00\x00\x01"), distances=[Distance(256), Distance(255)], @@ -482,14 +477,6 @@ def test_official_nodes_message_rlp_encoding(self): NODES format: [request-id, total, [enrs...]] Message type byte 0x04 prepended. """ - from lean_spec.subspecs.networking.discovery.codec import encode_message - from lean_spec.subspecs.networking.discovery.messages import ( - MessageType, - Nodes, - RequestId, - ) - from lean_spec.types.uint import Uint8 - nodes = Nodes( request_id=RequestId(data=b"\x00\x00\x00\x01"), total=Uint8(1), @@ -517,12 +504,6 @@ def test_message_packet_header_structure(self): - nonce: 12 bytes - authdata-size: 2 bytes """ - from lean_spec.subspecs.networking.discovery.packet import ( - STATIC_HEADER_SIZE, - encode_message_authdata, - encode_packet, - ) - nonce = bytes(12) encryption_key = bytes(16) message = b"\x01\xc2\x01\x01" @@ -550,13 +531,6 @@ def test_whoareyou_packet_header_structure(self): - authdata: id-nonce (16) + enr-seq (8) = 24 bytes - no message payload """ - from lean_spec.subspecs.networking.discovery.packet import ( - STATIC_HEADER_SIZE, - WHOAREYOU_AUTHDATA_SIZE, - encode_packet, - encode_whoareyou_authdata, - ) - nonce = bytes(12) id_nonce = bytes(16) enr_seq = 0 @@ -585,13 +559,6 @@ def test_handshake_packet_header_structure(self): - authdata: src-id (32) + sig-size (1) + eph-key-size (1) + sig + eph-key + [record] - encrypted message """ - from lean_spec.subspecs.networking.discovery.packet import ( - HANDSHAKE_HEADER_SIZE, - STATIC_HEADER_SIZE, - encode_handshake_authdata, - encode_packet, - ) - nonce = bytes(12) encryption_key = bytes(16) message = b"\x01\xc2\x01\x01" @@ -719,9 +686,6 @@ def test_aes_gcm_large_plaintext(self): def test_aes_gcm_wrong_key_fails_decryption(self): """AES-GCM decryption fails with wrong key.""" - import pytest - from cryptography.exceptions import InvalidTag - wrong_key = Bytes16(bytes.fromhex("00000000000000001a85107ac686990b")) aad = bytes(32) plaintext = b"secret message" @@ -734,9 +698,6 @@ def test_aes_gcm_wrong_key_fails_decryption(self): def test_aes_gcm_wrong_aad_fails_decryption(self): """AES-GCM decryption fails with wrong AAD.""" - import pytest - from cryptography.exceptions import InvalidTag - aad = bytes(32) wrong_aad = bytes([0xFF] * 32) plaintext = b"secret message" @@ -749,9 +710,6 @@ def test_aes_gcm_wrong_aad_fails_decryption(self): def test_aes_gcm_tampered_ciphertext_fails(self): """AES-GCM decryption fails with tampered ciphertext.""" - import pytest - from cryptography.exceptions import InvalidTag - aad = bytes(32) plaintext = b"secret message" @@ -832,9 +790,6 @@ class TestRoutingWithTestVectorNodeIds: def test_xor_distance_is_symmetric(self): """XOR distance between test vector nodes is symmetric and non-zero.""" - from lean_spec.subspecs.networking.discovery.routing import xor_distance - from lean_spec.subspecs.networking.types import NodeId - node_a = NodeId(NODE_A_ID) node_b = NodeId(NODE_B_ID) @@ -844,10 +799,6 @@ def test_xor_distance_is_symmetric(self): def test_log2_distance_is_high(self): """Log2 distance between test vector nodes is high (differ in high bits).""" - from lean_spec.subspecs.networking.discovery.messages import Distance - from lean_spec.subspecs.networking.discovery.routing import log2_distance - from lean_spec.subspecs.networking.types import NodeId - node_a = NodeId(NODE_A_ID) node_b = NodeId(NODE_B_ID) diff --git a/tests/lean_spec/subspecs/networking/enr/test_enr.py b/tests/lean_spec/subspecs/networking/enr/test_enr.py index 9c912ab8..c59ceccb 100644 --- a/tests/lean_spec/subspecs/networking/enr/test_enr.py +++ b/tests/lean_spec/subspecs/networking/enr/test_enr.py @@ -9,11 +9,22 @@ from __future__ import annotations +import base64 + import pytest +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, + decode_dss_signature, +) from lean_spec.subspecs.networking.enr import ENR, keys from lean_spec.subspecs.networking.enr.enr import ENR_PREFIX -from lean_spec.types import Bytes64, Uint64 +from lean_spec.types import Bytes64, SSZValueError, Uint64 +from lean_spec.types.byte_arrays import Bytes4 +from lean_spec.types.rlp import encode_rlp # From: https://eips.ethereum.org/EIPS/eip-778 # @@ -116,10 +127,6 @@ def test_official_enr_node_id(self) -> None: The hash is computed over the 64-byte x||y coordinates, excluding the 0x04 uncompressed point prefix. """ - from Crypto.Hash import keccak - from cryptography.hazmat.primitives import serialization - from cryptography.hazmat.primitives.asymmetric import ec - enr = ENR.from_string(OFFICIAL_ENR_STRING) # Get the compressed 33-byte secp256k1 public key @@ -195,10 +202,6 @@ class TestRLPStructureValidation: def test_minimum_fields_required(self) -> None: """ENR must have at least signature and seq.""" # Create RLP for just signature (missing seq) - import base64 - - from lean_spec.types.rlp import encode_rlp - # RLP list with only signature rlp_data = encode_rlp([b"\x00" * 64]) b64_content = base64.urlsafe_b64encode(rlp_data).decode("utf-8").rstrip("=") @@ -208,10 +211,6 @@ def test_minimum_fields_required(self) -> None: def test_odd_number_of_kv_pairs_rejected(self) -> None: """ENR key/value pairs must be even count.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - # [signature, seq, key1] - odd number after signature/seq rlp_data = encode_rlp([b"\x00" * 64, b"\x01", b"id"]) b64_content = base64.urlsafe_b64encode(rlp_data).decode("utf-8").rstrip("=") @@ -221,8 +220,6 @@ def test_odd_number_of_kv_pairs_rejected(self) -> None: def test_empty_rlp_rejected(self) -> None: """Empty RLP data is rejected.""" - import base64 - b64_content = base64.urlsafe_b64encode(b"").decode("utf-8").rstrip("=") with pytest.raises(ValueError, match=r"Invalid RLP"): @@ -230,8 +227,6 @@ def test_empty_rlp_rejected(self) -> None: def test_malformed_rlp_rejected(self) -> None: """Malformed RLP is rejected.""" - import base64 - # Invalid RLP: truncated list malformed = bytes([0xC5, 0x01, 0x02]) # Claims 5 bytes but only has 2 b64_content = base64.urlsafe_b64encode(malformed).decode("utf-8").rstrip("=") @@ -241,10 +236,6 @@ def test_malformed_rlp_rejected(self) -> None: def test_valid_minimal_enr(self) -> None: """Minimal valid ENR with only required fields parses.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - # [signature(64), seq(1), "id", "v4", "secp256k1", pubkey(33)] rlp_data = encode_rlp( [ @@ -476,10 +467,6 @@ def test_is_valid_returns_false_for_wrong_pubkey_length(self) -> None: def test_construction_fails_for_wrong_signature_length(self) -> None: """ENR construction fails when signature is not exactly 64 bytes.""" - import pytest - - from lean_spec.types import SSZValueError - # 63 bytes should fail - Bytes64 enforces exactly 64 bytes with pytest.raises(SSZValueError, match="requires exactly 64 bytes"): ENR( @@ -649,10 +636,6 @@ class TestEdgeCases: def test_enr_with_only_required_fields(self) -> None: """ENR with minimum required fields is valid.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - rlp_data = encode_rlp( [ b"\x00" * 64, # signature @@ -672,10 +655,6 @@ def test_enr_with_only_required_fields(self) -> None: def test_enr_with_ipv6_only(self) -> None: """ENR with IPv6 but no IPv4 parses correctly.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - ipv6_bytes = bytes.fromhex("20010db8000000000000000000000001") # 2001:db8::1 rlp_data = encode_rlp( [ @@ -705,10 +684,6 @@ def test_enr_with_ipv6_only(self) -> None: def test_enr_with_udp_port(self) -> None: """ENR with UDP port generates QUIC multiaddr correctly.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - rlp_data = encode_rlp( [ b"\x00" * 64, @@ -731,10 +706,6 @@ def test_enr_with_udp_port(self) -> None: def test_sequence_number_zero(self) -> None: """ENR with sequence number 0 is valid.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - rlp_data = encode_rlp( [ b"\x00" * 64, @@ -752,10 +723,6 @@ def test_sequence_number_zero(self) -> None: def test_large_sequence_number(self) -> None: """ENR with large sequence number parses correctly.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - large_seq = (2**32).to_bytes(5, "big") rlp_data = encode_rlp( [ @@ -794,8 +761,6 @@ class TestEth2DataProperty: def test_eth2_data_parses_from_enr(self) -> None: """eth2_data property parses 16-byte eth2 key.""" - from lean_spec.types.byte_arrays import Bytes4 - # 4 bytes fork_digest + 4 bytes next_fork_version + 8 bytes next_fork_epoch eth2_bytes = b"\x12\x34\x56\x78" + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x01" enr = ENR( @@ -996,10 +961,6 @@ class TestMaxSizeEnforcement: def test_enr_exactly_300_bytes_succeeds(self) -> None: """ENR with exactly 300 bytes RLP parses successfully.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - # Build an ENR that is exactly 300 bytes # Start with minimal structure and add padding in a value signature = b"\x00" * 64 @@ -1066,10 +1027,6 @@ def test_enr_exactly_300_bytes_succeeds(self) -> None: def test_enr_301_bytes_rejected(self) -> None: """ENR with 301 bytes RLP is rejected.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - # Build an ENR that is exactly 301 bytes signature = b"\x00" * 64 seq = b"\x01" @@ -1132,10 +1089,6 @@ class TestKeyOrderingEnforcement: def test_sorted_keys_accepted(self) -> None: """ENR with lexicographically sorted keys parses successfully.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - # Keys in sorted order: id, ip, secp256k1 rlp = encode_rlp( [ @@ -1155,10 +1108,6 @@ def test_sorted_keys_accepted(self) -> None: def test_unsorted_keys_rejected(self) -> None: """ENR with unsorted keys is rejected.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - # Keys out of order: secp256k1 before id rlp = encode_rlp( [ @@ -1177,10 +1126,6 @@ def test_unsorted_keys_rejected(self) -> None: def test_duplicate_keys_rejected(self) -> None: """ENR with duplicate keys is rejected.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - # Duplicate "id" key rlp = encode_rlp( [ @@ -1213,10 +1158,6 @@ def test_roundtrip_official_enr(self) -> None: def test_roundtrip_preserves_all_fields(self) -> None: """Round-trip preserves all ENR fields.""" - import base64 - - from lean_spec.types.rlp import encode_rlp - rlp = encode_rlp( [ b"\xab" * 64, # signature @@ -1267,16 +1208,6 @@ def test_official_enr_signature_verifies(self) -> None: def test_self_signed_enr_verifies(self) -> None: """ENR signed with cryptography library verifies correctly.""" - 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, - decode_dss_signature, - ) - - from lean_spec.types.rlp import encode_rlp - # Generate a test keypair using cryptography library. private_key = ec.generate_private_key(ec.SECP256K1()) public_key = private_key.public_key() diff --git a/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py b/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py index 26504a2c..9b7574f4 100644 --- a/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py +++ b/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py @@ -3,6 +3,7 @@ import pytest from lean_spec.subspecs.networking import PeerId +from lean_spec.subspecs.networking.client.event_source import GossipHandler from lean_spec.subspecs.networking.gossipsub import ( ControlGraft, ControlIDontWant, @@ -18,6 +19,39 @@ parse_topic_string, ) from lean_spec.subspecs.networking.gossipsub.mesh import FanoutEntry, MeshState, TopicMesh +from lean_spec.subspecs.networking.gossipsub.rpc import ( + RPC, + SubOpts, + create_graft_rpc, + create_ihave_rpc, + create_iwant_rpc, + create_prune_rpc, + create_publish_rpc, + create_subscription_rpc, +) +from lean_spec.subspecs.networking.gossipsub.rpc import ( + ControlGraft as RPCControlGraft, +) +from lean_spec.subspecs.networking.gossipsub.rpc import ( + ControlIDontWant as RPCControlIDontWant, +) +from lean_spec.subspecs.networking.gossipsub.rpc import ( + ControlIHave as RPCControlIHave, +) +from lean_spec.subspecs.networking.gossipsub.rpc import ( + ControlIWant as RPCControlIWant, +) +from lean_spec.subspecs.networking.gossipsub.rpc import ( + ControlMessage as RPCControlMessage, +) +from lean_spec.subspecs.networking.gossipsub.rpc import ( + ControlPrune as RPCControlPrune, +) +from lean_spec.subspecs.networking.gossipsub.rpc import ( + Message as RPCMessage, +) +from lean_spec.subspecs.networking.varint import decode_varint, encode_varint +from lean_spec.types import Bytes20 def peer(name: str) -> PeerId: @@ -63,8 +97,6 @@ def test_prune_creation(self) -> None: def test_ihave_creation(self) -> None: """Test IHAVE message creation.""" - from lean_spec.types import Bytes20 - msg_ids = [Bytes20(b"12345678901234567890"), Bytes20(b"abcdefghijklmnopqrst")] ihave = ControlIHave(topic_id="test_topic", message_ids=msg_ids) @@ -73,8 +105,6 @@ def test_ihave_creation(self) -> None: def test_iwant_creation(self) -> None: """Test IWANT message creation.""" - from lean_spec.types import Bytes20 - msg_ids = [Bytes20(b"12345678901234567890")] iwant = ControlIWant(message_ids=msg_ids) @@ -82,8 +112,6 @@ def test_iwant_creation(self) -> None: def test_idontwant_creation(self) -> None: """Test IDONTWANT message creation (v1.2).""" - from lean_spec.types import Bytes20 - msg_ids = [Bytes20(b"12345678901234567890")] idontwant = ControlIDontWant(message_ids=msg_ids) @@ -129,8 +157,6 @@ def test_validate_fork_success(self) -> None: def test_validate_fork_raises_on_mismatch(self) -> None: """Test validate_fork raises ForkMismatchError on mismatch.""" - from lean_spec.subspecs.networking.gossipsub import ForkMismatchError - topic = GossipTopic(kind=TopicKind.BLOCK, fork_digest="0x12345678") with pytest.raises(ForkMismatchError) as exc_info: topic.validate_fork("0xdeadbeef") @@ -149,8 +175,6 @@ def test_from_string_validated_success(self) -> None: def test_from_string_validated_raises_on_mismatch(self) -> None: """Test from_string_validated raises ForkMismatchError on mismatch.""" - from lean_spec.subspecs.networking.gossipsub import ForkMismatchError - with pytest.raises(ForkMismatchError): GossipTopic.from_string_validated( "/leanconsensus/0x12345678/block/ssz_snappy", @@ -450,8 +474,6 @@ class TestRPCProtobufEncoding: def test_varint_encoding(self) -> None: """Test varint encoding matches protobuf spec.""" - from lean_spec.subspecs.networking.varint import encode_varint - # Single byte varints (0-127) assert encode_varint(0) == b"\x00" assert encode_varint(1) == b"\x01" @@ -467,8 +489,6 @@ def test_varint_encoding(self) -> None: def test_varint_decoding(self) -> None: """Test varint decoding matches protobuf spec.""" - from lean_spec.subspecs.networking.varint import decode_varint - # Single byte value, pos = decode_varint(b"\x00", 0) assert value == 0 @@ -489,8 +509,6 @@ def test_varint_decoding(self) -> None: def test_varint_roundtrip(self) -> None: """Test varint encode/decode roundtrip.""" - from lean_spec.subspecs.networking.varint import decode_varint, encode_varint - test_values = [0, 1, 127, 128, 255, 256, 16383, 16384, 2097151, 268435455] for value in test_values: encoded = encode_varint(value) @@ -499,8 +517,6 @@ def test_varint_roundtrip(self) -> None: def test_subopts_encode_decode(self) -> None: """Test SubOpts (subscription) encoding/decoding.""" - from lean_spec.subspecs.networking.gossipsub.rpc import SubOpts - # Subscribe sub = SubOpts(subscribe=True, topic_id="/leanconsensus/0x12345678/block/ssz_snappy") encoded = sub.encode() @@ -519,8 +535,6 @@ def test_subopts_encode_decode(self) -> None: def test_message_encode_decode(self) -> None: """Test Message encoding/decoding.""" - from lean_spec.subspecs.networking.gossipsub.rpc import Message as RPCMessage - msg = RPCMessage( from_peer=b"peer123", data=b"hello world", @@ -541,8 +555,6 @@ def test_message_encode_decode(self) -> None: def test_message_minimal(self) -> None: """Test Message with only required fields.""" - from lean_spec.subspecs.networking.gossipsub.rpc import Message as RPCMessage - msg = RPCMessage(topic="/test/topic", data=b"payload") encoded = msg.encode() decoded = RPCMessage.decode(encoded) @@ -554,8 +566,6 @@ def test_message_minimal(self) -> None: def test_control_graft_encode_decode(self) -> None: """Test ControlGraft encoding/decoding.""" - from lean_spec.subspecs.networking.gossipsub.rpc import ControlGraft as RPCControlGraft - graft = RPCControlGraft(topic_id="/test/blocks") encoded = graft.encode() decoded = RPCControlGraft.decode(encoded) @@ -564,8 +574,6 @@ def test_control_graft_encode_decode(self) -> None: def test_control_prune_encode_decode(self) -> None: """Test ControlPrune encoding/decoding with backoff.""" - from lean_spec.subspecs.networking.gossipsub.rpc import ControlPrune as RPCControlPrune - prune = RPCControlPrune(topic_id="/test/blocks", backoff=60) encoded = prune.encode() decoded = RPCControlPrune.decode(encoded) @@ -575,8 +583,6 @@ def test_control_prune_encode_decode(self) -> None: def test_control_ihave_encode_decode(self) -> None: """Test ControlIHave encoding/decoding.""" - from lean_spec.subspecs.networking.gossipsub.rpc import ControlIHave as RPCControlIHave - msg_ids = [b"msgid1234567890ab", b"msgid2345678901bc", b"msgid3456789012cd"] ihave = RPCControlIHave(topic_id="/test/blocks", message_ids=msg_ids) encoded = ihave.encode() @@ -587,8 +593,6 @@ def test_control_ihave_encode_decode(self) -> None: def test_control_iwant_encode_decode(self) -> None: """Test ControlIWant encoding/decoding.""" - from lean_spec.subspecs.networking.gossipsub.rpc import ControlIWant as RPCControlIWant - msg_ids = [b"msgid1234567890ab", b"msgid2345678901bc"] iwant = RPCControlIWant(message_ids=msg_ids) encoded = iwant.encode() @@ -598,10 +602,6 @@ def test_control_iwant_encode_decode(self) -> None: def test_control_idontwant_encode_decode(self) -> None: """Test ControlIDontWant encoding/decoding (v1.2).""" - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlIDontWant as RPCControlIDontWant, - ) - msg_ids = [b"msgid1234567890ab"] idontwant = RPCControlIDontWant(message_ids=msg_ids) encoded = idontwant.encode() @@ -611,19 +611,6 @@ def test_control_idontwant_encode_decode(self) -> None: def test_control_message_aggregate(self) -> None: """Test ControlMessage with multiple control types.""" - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlGraft as RPCControlGraft, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlIHave as RPCControlIHave, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlMessage as RPCControlMessage, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlPrune as RPCControlPrune, - ) - ctrl = RPCControlMessage( graft=[RPCControlGraft(topic_id="/topic1")], prune=[RPCControlPrune(topic_id="/topic2", backoff=30)], @@ -642,8 +629,6 @@ def test_control_message_aggregate(self) -> None: def test_rpc_subscription_only(self) -> None: """Test RPC with only subscriptions.""" - from lean_spec.subspecs.networking.gossipsub.rpc import RPC, SubOpts - rpc = RPC( subscriptions=[ SubOpts(subscribe=True, topic_id="/topic1"), @@ -661,9 +646,6 @@ def test_rpc_subscription_only(self) -> None: def test_rpc_publish_only(self) -> None: """Test RPC with only published messages.""" - from lean_spec.subspecs.networking.gossipsub.rpc import RPC - from lean_spec.subspecs.networking.gossipsub.rpc import Message as RPCMessage - rpc = RPC( publish=[ RPCMessage(topic="/blocks", data=b"block_data_1"), @@ -680,16 +662,6 @@ def test_rpc_publish_only(self) -> None: def test_rpc_control_only(self) -> None: """Test RPC with only control messages.""" - from lean_spec.subspecs.networking.gossipsub.rpc import ( - RPC, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlGraft as RPCControlGraft, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlMessage as RPCControlMessage, - ) - rpc = RPC(control=RPCControlMessage(graft=[RPCControlGraft(topic_id="/blocks")])) encoded = rpc.encode() decoded = RPC.decode(encoded) @@ -700,23 +672,6 @@ def test_rpc_control_only(self) -> None: def test_rpc_full_message(self) -> None: """Test RPC with all message types (full gossipsub exchange).""" - from lean_spec.subspecs.networking.gossipsub.rpc import ( - RPC, - SubOpts, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlGraft as RPCControlGraft, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlIHave as RPCControlIHave, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - ControlMessage as RPCControlMessage, - ) - from lean_spec.subspecs.networking.gossipsub.rpc import ( - Message as RPCMessage, - ) - rpc = RPC( subscriptions=[SubOpts(subscribe=True, topic_id="/blocks")], publish=[RPCMessage(topic="/blocks", data=b"block_payload")], @@ -741,8 +696,6 @@ def test_rpc_full_message(self) -> None: def test_rpc_empty_check(self) -> None: """Test RPC is_empty method.""" - from lean_spec.subspecs.networking.gossipsub.rpc import RPC, SubOpts - empty_rpc = RPC() assert empty_rpc.is_empty() @@ -751,15 +704,6 @@ def test_rpc_empty_check(self) -> None: def test_rpc_helper_functions(self) -> None: """Test RPC creation helper functions.""" - from lean_spec.subspecs.networking.gossipsub.rpc import ( - create_graft_rpc, - create_ihave_rpc, - create_iwant_rpc, - create_prune_rpc, - create_publish_rpc, - create_subscription_rpc, - ) - # Subscription RPC sub_rpc = create_subscription_rpc(["/topic1", "/topic2"], subscribe=True) assert len(sub_rpc.subscriptions) == 2 @@ -798,8 +742,6 @@ def test_wire_format_compatibility(self) -> None: This test verifies that our encoding produces the same bytes as a reference implementation would for simple cases. """ - from lean_spec.subspecs.networking.gossipsub.rpc import RPC, SubOpts - # A subscription RPC with a simple topic rpc = RPC(subscriptions=[SubOpts(subscribe=True, topic_id="test")]) encoded = rpc.encode() @@ -816,9 +758,6 @@ def test_wire_format_compatibility(self) -> None: def test_large_message_encoding(self) -> None: """Test encoding of large messages (typical block size).""" - from lean_spec.subspecs.networking.gossipsub.rpc import RPC - from lean_spec.subspecs.networking.gossipsub.rpc import Message as RPCMessage - # Simulate a large block payload (100KB) large_data = b"x" * 100_000 @@ -836,8 +775,6 @@ class TestGossipHandlerForkValidation: def test_decode_message_rejects_wrong_fork(self) -> None: """GossipHandler.decode_message() raises ForkMismatchError for wrong fork.""" - from lean_spec.subspecs.networking.client.event_source import GossipHandler - handler = GossipHandler(fork_digest="0x12345678") # Topic with different fork_digest @@ -851,8 +788,6 @@ def test_decode_message_rejects_wrong_fork(self) -> None: def test_get_topic_rejects_wrong_fork(self) -> None: """GossipHandler.get_topic() raises ForkMismatchError for wrong fork.""" - from lean_spec.subspecs.networking.client.event_source import GossipHandler - handler = GossipHandler(fork_digest="0x12345678") # Topic with different fork_digest @@ -866,8 +801,6 @@ def test_get_topic_rejects_wrong_fork(self) -> None: def test_get_topic_accepts_matching_fork(self) -> None: """GossipHandler.get_topic() returns topic for matching fork.""" - from lean_spec.subspecs.networking.client.event_source import GossipHandler - handler = GossipHandler(fork_digest="0x12345678") # Topic with matching fork_digest diff --git a/tests/lean_spec/subspecs/networking/reqresp/test_handler.py b/tests/lean_spec/subspecs/networking/reqresp/test_handler.py index a92bb051..5393df3d 100644 --- a/tests/lean_spec/subspecs/networking/reqresp/test_handler.py +++ b/tests/lean_spec/subspecs/networking/reqresp/test_handler.py @@ -13,6 +13,7 @@ ) from lean_spec.subspecs.networking.reqresp.handler import ( REQRESP_PROTOCOL_IDS, + REQUEST_TIMEOUT_SECONDS, BlockLookup, DefaultRequestHandler, ReqRespServer, @@ -1200,14 +1201,10 @@ class TestRequestTimeoutConstant: def test_timeout_is_positive(self) -> None: """Request timeout is a positive number.""" - from lean_spec.subspecs.networking.reqresp.handler import REQUEST_TIMEOUT_SECONDS - assert REQUEST_TIMEOUT_SECONDS > 0 def test_timeout_is_reasonable(self) -> None: """Request timeout is within reasonable bounds.""" - from lean_spec.subspecs.networking.reqresp.handler import REQUEST_TIMEOUT_SECONDS - # Should be at least a few seconds assert REQUEST_TIMEOUT_SECONDS >= 1.0 # Should not be excessively long diff --git a/tests/lean_spec/subspecs/networking/test_peer.py b/tests/lean_spec/subspecs/networking/test_peer.py index 9d45e4b5..2f8a57cc 100644 --- a/tests/lean_spec/subspecs/networking/test_peer.py +++ b/tests/lean_spec/subspecs/networking/test_peer.py @@ -1,13 +1,16 @@ """Tests for minimal peer module.""" -from typing import TYPE_CHECKING +import time +from lean_spec.subspecs.containers.checkpoint import Checkpoint +from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.networking import PeerId +from lean_spec.subspecs.networking.enr import ENR +from lean_spec.subspecs.networking.enr.eth2 import FAR_FUTURE_EPOCH from lean_spec.subspecs.networking.peer import Direction, PeerInfo +from lean_spec.subspecs.networking.reqresp import Status from lean_spec.subspecs.networking.types import ConnectionState, GoodbyeReason - -if TYPE_CHECKING: - from lean_spec.subspecs.networking.enr import ENR +from lean_spec.types import Bytes32, Bytes64, Uint64 def peer(name: str) -> PeerId: @@ -91,8 +94,6 @@ def test_update_last_seen(self) -> None: original_time = info.last_seen # Small delay to ensure time difference - import time - time.sleep(0.01) info.update_last_seen() @@ -102,12 +103,8 @@ def test_update_last_seen(self) -> None: class TestPeerInfoForkDigest: """Tests for PeerInfo fork_digest property.""" - def _make_enr_with_eth2(self, fork_digest_bytes: bytes) -> "ENR": + def _make_enr_with_eth2(self, fork_digest_bytes: bytes) -> ENR: """Create a minimal ENR with eth2 data for testing.""" - from lean_spec.subspecs.networking.enr import ENR - from lean_spec.subspecs.networking.enr.eth2 import FAR_FUTURE_EPOCH - from lean_spec.types import Bytes64, Uint64 - # Create eth2 bytes: fork_digest(4) + next_fork_version(4) + next_fork_epoch(8) eth2_bytes = ( fork_digest_bytes + fork_digest_bytes + int(FAR_FUTURE_EPOCH).to_bytes(8, "little") @@ -125,9 +122,6 @@ def test_fork_digest_none_without_enr(self) -> None: def test_fork_digest_none_without_eth2(self) -> None: """fork_digest returns None when ENR has no eth2 data.""" - from lean_spec.subspecs.networking.enr import ENR - from lean_spec.types import Bytes64, Uint64 - # ENR without eth2 key enr = ENR( signature=Bytes64(b"\x00" * 64), @@ -153,11 +147,6 @@ def test_enr_and_status_fields(self) -> None: def test_status_can_be_set(self) -> None: """Test that status can be set and read back.""" - from lean_spec.subspecs.containers.checkpoint import Checkpoint - from lean_spec.subspecs.containers.slot import Slot - from lean_spec.subspecs.networking.reqresp import Status - from lean_spec.types import Bytes32 - info = PeerInfo(peer_id=peer("test")) # Create a test status @@ -175,8 +164,6 @@ def test_status_can_be_set(self) -> None: def test_update_last_seen_updates_timestamp(self) -> None: """Test that update_last_seen updates the last_seen timestamp.""" - import time - info = PeerInfo(peer_id=peer("test")) original_time = info.last_seen diff --git a/tests/lean_spec/subspecs/networking/test_reqresp.py b/tests/lean_spec/subspecs/networking/test_reqresp.py index 08735233..d188687d 100644 --- a/tests/lean_spec/subspecs/networking/test_reqresp.py +++ b/tests/lean_spec/subspecs/networking/test_reqresp.py @@ -17,13 +17,20 @@ from __future__ import annotations +import hashlib + import pytest from lean_spec.subspecs.networking import ResponseCode +from lean_spec.subspecs.networking.config import MAX_PAYLOAD_SIZE from lean_spec.subspecs.networking.reqresp import ( + CONTEXT_BYTES_LENGTH, CodecError, + ForkDigestMismatchError, decode_request, encode_request, + prepend_context_bytes, + validate_context_bytes, ) from lean_spec.subspecs.networking.varint import ( VarintError, @@ -329,8 +336,6 @@ def test_varint_11_bytes_rejected(self) -> None: def test_payload_at_max_size(self) -> None: """Payload at exactly MAX_PAYLOAD_SIZE is accepted.""" - from lean_spec.subspecs.networking.config import MAX_PAYLOAD_SIZE - # Create payload at exactly MAX_PAYLOAD_SIZE ssz_data = b"X" * MAX_PAYLOAD_SIZE @@ -345,8 +350,6 @@ def test_payload_at_max_size(self) -> None: def test_payload_over_max_size_rejected_on_encode(self) -> None: """Payload exceeding MAX_PAYLOAD_SIZE is rejected on encode.""" - from lean_spec.subspecs.networking.config import MAX_PAYLOAD_SIZE - oversized = b"X" * (MAX_PAYLOAD_SIZE + 1) with pytest.raises(CodecError, match="too large"): @@ -354,8 +357,6 @@ def test_payload_over_max_size_rejected_on_encode(self) -> None: def test_declared_length_over_max_rejected_on_decode(self) -> None: """Declared length exceeding MAX_PAYLOAD_SIZE is rejected on decode.""" - from lean_spec.subspecs.networking.config import MAX_PAYLOAD_SIZE - # Encode a small request, then modify the length prefix valid_encoded = encode_request(b"test") @@ -369,8 +370,6 @@ def test_declared_length_over_max_rejected_on_decode(self) -> None: def test_response_payload_at_max_size(self) -> None: """Response payload at exactly MAX_PAYLOAD_SIZE is accepted.""" - from lean_spec.subspecs.networking.config import MAX_PAYLOAD_SIZE - ssz_data = b"Y" * MAX_PAYLOAD_SIZE encoded = ResponseCode.SUCCESS.encode(ssz_data) @@ -594,8 +593,6 @@ def test_highly_compressible_data(self) -> None: def test_incompressible_data(self) -> None: """Incompressible (random-like) data roundtrips correctly.""" # Create pseudo-random data that doesn't compress well - import hashlib - incompressible = b"" for i in range(1000): incompressible += hashlib.sha256(str(i).encode()).digest() @@ -618,8 +615,6 @@ class TestContextBytesValidation: def test_validate_context_bytes_success(self) -> None: """Valid context bytes are validated and stripped.""" - from lean_spec.subspecs.networking.reqresp import validate_context_bytes - fork_digest = b"\x12\x34\x56\x78" payload = b"block data here" data = fork_digest + payload @@ -629,11 +624,6 @@ def test_validate_context_bytes_success(self) -> None: def test_validate_context_bytes_mismatch(self) -> None: """Mismatched context bytes raise ForkDigestMismatchError.""" - from lean_spec.subspecs.networking.reqresp import ( - ForkDigestMismatchError, - validate_context_bytes, - ) - expected_fork = b"\x12\x34\x56\x78" actual_fork = b"\xde\xad\xbe\xef" payload = b"block data" @@ -647,8 +637,6 @@ def test_validate_context_bytes_mismatch(self) -> None: def test_validate_context_bytes_too_short(self) -> None: """Data shorter than context bytes raises CodecError.""" - from lean_spec.subspecs.networking.reqresp import validate_context_bytes - fork_digest = b"\x12\x34\x56\x78" too_short = b"\x12\x34" # Only 2 bytes @@ -657,8 +645,6 @@ def test_validate_context_bytes_too_short(self) -> None: def test_validate_context_bytes_exactly_4_bytes(self) -> None: """Data of exactly 4 bytes (context only, no payload) works.""" - from lean_spec.subspecs.networking.reqresp import validate_context_bytes - fork_digest = b"\x12\x34\x56\x78" data = fork_digest # No payload, just context bytes @@ -667,8 +653,6 @@ def test_validate_context_bytes_exactly_4_bytes(self) -> None: def test_prepend_context_bytes(self) -> None: """Context bytes are correctly prepended to payload.""" - from lean_spec.subspecs.networking.reqresp import prepend_context_bytes - fork_digest = b"\x12\x34\x56\x78" payload = b"block data" @@ -678,8 +662,6 @@ def test_prepend_context_bytes(self) -> None: def test_prepend_context_bytes_wrong_length(self) -> None: """Prepending context bytes with wrong length raises ValueError.""" - from lean_spec.subspecs.networking.reqresp import prepend_context_bytes - invalid_fork = b"\x12\x34\x56" # Only 3 bytes payload = b"block data" @@ -688,11 +670,6 @@ def test_prepend_context_bytes_wrong_length(self) -> None: def test_context_bytes_roundtrip(self) -> None: """Prepend and validate context bytes roundtrip.""" - from lean_spec.subspecs.networking.reqresp import ( - prepend_context_bytes, - validate_context_bytes, - ) - fork_digest = b"\xab\xcd\xef\x01" original_payload = b"some response data" @@ -706,8 +683,6 @@ def test_context_bytes_roundtrip(self) -> None: def test_fork_digest_mismatch_error_message(self) -> None: """ForkDigestMismatchError has informative message.""" - from lean_spec.subspecs.networking.reqresp import ForkDigestMismatchError - expected = b"\x12\x34\x56\x78" actual = b"\xde\xad\xbe\xef" @@ -717,6 +692,4 @@ def test_fork_digest_mismatch_error_message(self) -> None: def test_context_bytes_length_constant(self) -> None: """CONTEXT_BYTES_LENGTH constant is 4.""" - from lean_spec.subspecs.networking.reqresp import CONTEXT_BYTES_LENGTH - assert CONTEXT_BYTES_LENGTH == 4 diff --git a/tests/lean_spec/subspecs/networking/transport/multistream/test_negotiation.py b/tests/lean_spec/subspecs/networking/transport/multistream/test_negotiation.py index 0418f521..7d20af4a 100644 --- a/tests/lean_spec/subspecs/networking/transport/multistream/test_negotiation.py +++ b/tests/lean_spec/subspecs/networking/transport/multistream/test_negotiation.py @@ -32,6 +32,7 @@ StreamReaderProtocol, StreamWriterProtocol, ) +from lean_spec.subspecs.networking.varint import decode_varint, encode_varint class TestConstants: @@ -428,8 +429,6 @@ async def wait_closed(self) -> None: async def _write_message(writer: StreamWriterProtocol, message: str) -> None: """Write a multistream message.""" - from lean_spec.subspecs.networking.varint import encode_varint - payload = message.encode("utf-8") + b"\n" length_prefix = encode_varint(len(payload)) writer.write(length_prefix + payload) @@ -448,8 +447,6 @@ async def _read_message(reader: StreamReaderProtocol) -> str: if byte[0] & 0x80 == 0: break - from lean_spec.subspecs.networking.varint import decode_varint - length, _ = decode_varint(bytes(length_bytes)) # Read payload diff --git a/tests/lean_spec/subspecs/sync/test_service.py b/tests/lean_spec/subspecs/sync/test_service.py index 6604d7c9..85f580ba 100644 --- a/tests/lean_spec/subspecs/sync/test_service.py +++ b/tests/lean_spec/subspecs/sync/test_service.py @@ -9,6 +9,7 @@ from lean_spec.subspecs.containers.validator import ValidatorIndex from lean_spec.subspecs.networking import PeerId from lean_spec.subspecs.networking.reqresp.message import Status +from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.sync.service import SyncService from lean_spec.subspecs.sync.states import SyncState from lean_spec.types import Bytes32 @@ -208,8 +209,6 @@ async def test_caches_orphan_in_syncing_state( parent_root=Bytes32(b"\x01" * 32), state_root=Bytes32.zero(), ) - from lean_spec.subspecs.ssz.hash import hash_tree_root - block_root = hash_tree_root(block.message.block) await sync_service.on_gossip_block(block, peer_id) diff --git a/tests/lean_spec/subspecs/validator/test_service.py b/tests/lean_spec/subspecs/validator/test_service.py index f8b3a888..5ae5bcfc 100644 --- a/tests/lean_spec/subspecs/validator/test_service.py +++ b/tests/lean_spec/subspecs/validator/test_service.py @@ -8,6 +8,7 @@ from consensus_testing.keys import XmssKeyManager, get_shared_key_manager from lean_spec.subspecs.chain.clock import SlotClock +from lean_spec.subspecs.chain.config import MILLISECONDS_PER_INTERVAL from lean_spec.subspecs.containers import ( AttestationData, Block, @@ -15,6 +16,7 @@ SignedBlockWithAttestation, ValidatorIndex, ) +from lean_spec.subspecs.containers.attestation import AggregationBits from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root @@ -24,7 +26,7 @@ from lean_spec.subspecs.validator import ValidatorRegistry, ValidatorService from lean_spec.subspecs.validator.registry import ValidatorEntry from lean_spec.subspecs.xmss import TARGET_SIGNATURE_SCHEME -from lean_spec.subspecs.xmss.aggregation import SignatureKey +from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof, SignatureKey from lean_spec.types import Bytes32, Uint64 from tests.lean_spec.helpers import TEST_VALIDATOR_ID, MockNetworkRequester, make_store @@ -190,8 +192,6 @@ async def test_sleep_until_next_interval_mid_interval( sync_service: SyncService, ) -> None: """Sleep duration is calculated correctly mid-interval.""" - from lean_spec.subspecs.chain.config import MILLISECONDS_PER_INTERVAL - genesis = Uint64(1000) interval_seconds = float(MILLISECONDS_PER_INTERVAL) / 1000.0 # Half way into first interval @@ -699,10 +699,6 @@ async def test_block_includes_pending_attestations( attestation_data = store.produce_attestation_data(Slot(0)) data_root = attestation_data.data_root_bytes() - # Simulate aggregated payloads for validators 3 and 4 - from lean_spec.subspecs.containers.attestation import AggregationBits - from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof - attestation_map: dict[ValidatorIndex, AttestationData] = {} signatures = [] participants = [ValidatorIndex(3), ValidatorIndex(4)] diff --git a/tests/lean_spec/subspecs/xmss/test_ssz_serialization.py b/tests/lean_spec/subspecs/xmss/test_ssz_serialization.py index 6da94c4e..6ac400f4 100644 --- a/tests/lean_spec/subspecs/xmss/test_ssz_serialization.py +++ b/tests/lean_spec/subspecs/xmss/test_ssz_serialization.py @@ -1,7 +1,7 @@ """Tests for SSZ serialization of XMSS types.""" from lean_spec.subspecs.xmss.constants import TEST_CONFIG -from lean_spec.subspecs.xmss.containers import PublicKey, Signature +from lean_spec.subspecs.xmss.containers import PublicKey, SecretKey, Signature from lean_spec.subspecs.xmss.interface import TEST_SIGNATURE_SCHEME from lean_spec.types import Bytes32, Uint64 @@ -63,8 +63,6 @@ def test_secret_key_ssz_roundtrip() -> None: sk_bytes = secret_key.encode_bytes() # Deserialize from bytes - from lean_spec.subspecs.xmss.containers import SecretKey - recovered_sk = SecretKey.decode_bytes(sk_bytes) # Verify the recovered secret key matches the original diff --git a/tests/lean_spec/subspecs/xmss/test_strict_types.py b/tests/lean_spec/subspecs/xmss/test_strict_types.py index 1bec45d0..a0e45af4 100644 --- a/tests/lean_spec/subspecs/xmss/test_strict_types.py +++ b/tests/lean_spec/subspecs/xmss/test_strict_types.py @@ -8,6 +8,7 @@ import pytest from pydantic import ValidationError +from lean_spec.subspecs.poseidon2.permutation import Poseidon2Params from lean_spec.subspecs.xmss.constants import PROD_CONFIG, TEST_CONFIG, XmssConfig from lean_spec.subspecs.xmss.interface import GeneralizedXmssScheme from lean_spec.subspecs.xmss.message_hash import PROD_MESSAGE_HASHER, MessageHasher @@ -327,7 +328,6 @@ def test_poseidon_accepts_exact_types(self) -> None: def test_poseidon_rejects_subclass_params16(self) -> None: """PoseidonXmss rejects Poseidon2Params subclass for params16.""" - from lean_spec.subspecs.poseidon2.permutation import Poseidon2Params class CustomParams(Poseidon2Params): pass @@ -340,7 +340,6 @@ class CustomParams(Poseidon2Params): def test_poseidon_rejects_subclass_params24(self) -> None: """PoseidonXmss rejects Poseidon2Params subclass for params24.""" - from lean_spec.subspecs.poseidon2.permutation import Poseidon2Params class CustomParams(Poseidon2Params): pass diff --git a/tests/lean_spec/test_cli.py b/tests/lean_spec/test_cli.py index 7e9b9820..2c92bb3b 100644 --- a/tests/lean_spec/test_cli.py +++ b/tests/lean_spec/test_cli.py @@ -15,11 +15,19 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import Prehashed, decode_dss_signature -from lean_spec.__main__ import create_anchor_block, is_enr_string, resolve_bootnode +from lean_spec.__main__ import ( + _init_from_checkpoint, + create_anchor_block, + is_enr_string, + resolve_bootnode, +) from lean_spec.subspecs.containers import Block, BlockBody from lean_spec.subspecs.containers.block.types import AggregatedAttestations from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.genesis import GenesisConfig +from lean_spec.subspecs.node import Node from lean_spec.subspecs.ssz.hash import hash_tree_root +from lean_spec.subspecs.sync.checkpoint_sync import CheckpointSyncError from lean_spec.types import Bytes32, Uint64 from lean_spec.types.rlp import encode_rlp from tests.lean_spec.helpers import make_genesis_state @@ -353,9 +361,6 @@ class TestInitFromCheckpoint: async def test_checkpoint_sync_genesis_time_mismatch_returns_none(self) -> None: """Returns None when checkpoint state genesis time differs from local config.""" - from lean_spec.__main__ import _init_from_checkpoint - from lean_spec.subspecs.genesis import GenesisConfig - # Arrange: Create checkpoint state with genesis_time=1000 checkpoint_state = make_genesis_state(num_validators=3, genesis_time=1000) @@ -394,9 +399,6 @@ async def test_checkpoint_sync_genesis_time_mismatch_returns_none(self) -> None: async def test_checkpoint_sync_verification_failure_returns_none(self) -> None: """Returns None when checkpoint state verification fails.""" - from lean_spec.__main__ import _init_from_checkpoint - from lean_spec.subspecs.genesis import GenesisConfig - # Arrange checkpoint_state = make_genesis_state(num_validators=3, genesis_time=1000) local_genesis = GenesisConfig.model_validate( @@ -432,10 +434,6 @@ async def test_checkpoint_sync_verification_failure_returns_none(self) -> None: async def test_checkpoint_sync_network_error_returns_none(self) -> None: """Returns None when network error occurs during fetch.""" - from lean_spec.__main__ import _init_from_checkpoint - from lean_spec.subspecs.genesis import GenesisConfig - from lean_spec.subspecs.sync.checkpoint_sync import CheckpointSyncError - # Arrange local_genesis = GenesisConfig.model_validate( { @@ -463,10 +461,6 @@ async def test_checkpoint_sync_network_error_returns_none(self) -> None: async def test_checkpoint_sync_success_returns_node(self) -> None: """Successful checkpoint sync returns initialized Node.""" - from lean_spec.__main__ import _init_from_checkpoint - from lean_spec.subspecs.genesis import GenesisConfig - from lean_spec.subspecs.node import Node - # Arrange: Create matching genesis times genesis_time = 1000 checkpoint_state = make_genesis_state(num_validators=3, genesis_time=genesis_time) @@ -510,10 +504,6 @@ async def test_checkpoint_sync_success_returns_node(self) -> None: async def test_checkpoint_sync_http_status_error_returns_none(self) -> None: """Returns None when HTTP status error occurs.""" - from lean_spec.__main__ import _init_from_checkpoint - from lean_spec.subspecs.genesis import GenesisConfig - from lean_spec.subspecs.sync.checkpoint_sync import CheckpointSyncError - # Arrange local_genesis = GenesisConfig.model_validate( { diff --git a/tests/lean_spec/types/test_uint.py b/tests/lean_spec/types/test_uint.py index 9a2a8249..c15117e6 100644 --- a/tests/lean_spec/types/test_uint.py +++ b/tests/lean_spec/types/test_uint.py @@ -1,6 +1,7 @@ """Unsigned Integer Type Tests.""" import io +import operator from typing import Any, Type import pytest @@ -308,8 +309,6 @@ def test_index_hex_bin_oct(uint_class: Type[BaseUint]) -> None: @pytest.mark.parametrize("uint_class", ALL_UINT_TYPES) def test_index_operator_index(uint_class: Type[BaseUint]) -> None: """Tests that operator.index() works with Uint types.""" - import operator - val = uint_class(42) assert operator.index(val) == 42 assert isinstance(operator.index(val), int) From 58515c2cf35320a05ee4d241d8e45c67f233ea21 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Tue, 10 Feb 2026 19:26:43 +0100 Subject: [PATCH 2/3] fix tests --- src/lean_spec/subspecs/sync/service.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/lean_spec/subspecs/sync/service.py b/src/lean_spec/subspecs/sync/service.py index fd6889c1..299791f6 100644 --- a/src/lean_spec/subspecs/sync/service.py +++ b/src/lean_spec/subspecs/sync/service.py @@ -52,7 +52,6 @@ from lean_spec.subspecs.forkchoice.store import Store from lean_spec.subspecs.networking.reqresp.message import Status from lean_spec.subspecs.networking.transport.peer_id import PeerId -from lean_spec.subspecs.node.helpers import is_aggregator from lean_spec.subspecs.ssz.hash import hash_tree_root from .backfill_sync import BackfillSync, NetworkRequester @@ -426,11 +425,11 @@ async def on_gossip_attestation( if not self._state.accepts_gossip: return - # Check if we are an aggregator - is_aggregator_role = is_aggregator( - self.store.validator_id, - node_is_aggregator=self.is_aggregator, - ) + # Check if we are an aggregator. + # + # A validator acts as an aggregator when it is active (has an ID) + # and the node operator has enabled aggregator mode. + is_aggregator_role = self.store.validator_id is not None and self.is_aggregator # Integrate the attestation into forkchoice state. # From dc6e4ff9ac7b1a15d04aff35b7feb976430cf6e2 Mon Sep 17 00:00:00 2001 From: Thomas Coratger Date: Tue, 10 Feb 2026 19:32:01 +0100 Subject: [PATCH 3/3] fix --- tests/lean_spec/test_cli.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/lean_spec/test_cli.py b/tests/lean_spec/test_cli.py index 2c92bb3b..fef1fa40 100644 --- a/tests/lean_spec/test_cli.py +++ b/tests/lean_spec/test_cli.py @@ -377,12 +377,12 @@ async def test_checkpoint_sync_genesis_time_mismatch_returns_none(self) -> None: with ( patch( - "lean_spec.subspecs.sync.checkpoint_sync.fetch_finalized_state", + "lean_spec.__main__.fetch_finalized_state", new_callable=AsyncMock, return_value=checkpoint_state, ), patch( - "lean_spec.subspecs.sync.checkpoint_sync.verify_checkpoint_state", + "lean_spec.__main__.verify_checkpoint_state", new_callable=AsyncMock, return_value=True, ), @@ -412,12 +412,12 @@ async def test_checkpoint_sync_verification_failure_returns_none(self) -> None: with ( patch( - "lean_spec.subspecs.sync.checkpoint_sync.fetch_finalized_state", + "lean_spec.__main__.fetch_finalized_state", new_callable=AsyncMock, return_value=checkpoint_state, ), patch( - "lean_spec.subspecs.sync.checkpoint_sync.verify_checkpoint_state", + "lean_spec.__main__.verify_checkpoint_state", new_callable=AsyncMock, return_value=False, # Verification fails ), @@ -445,7 +445,7 @@ async def test_checkpoint_sync_network_error_returns_none(self) -> None: mock_event_source = AsyncMock() with patch( - "lean_spec.subspecs.sync.checkpoint_sync.fetch_finalized_state", + "lean_spec.__main__.fetch_finalized_state", new_callable=AsyncMock, side_effect=CheckpointSyncError("Network error: connection refused"), ): @@ -478,12 +478,12 @@ async def test_checkpoint_sync_success_returns_node(self) -> None: with ( patch( - "lean_spec.subspecs.sync.checkpoint_sync.fetch_finalized_state", + "lean_spec.__main__.fetch_finalized_state", new_callable=AsyncMock, return_value=checkpoint_state, ), patch( - "lean_spec.subspecs.sync.checkpoint_sync.verify_checkpoint_state", + "lean_spec.__main__.verify_checkpoint_state", new_callable=AsyncMock, return_value=True, ), @@ -515,7 +515,7 @@ async def test_checkpoint_sync_http_status_error_returns_none(self) -> None: mock_event_source = AsyncMock() with patch( - "lean_spec.subspecs.sync.checkpoint_sync.fetch_finalized_state", + "lean_spec.__main__.fetch_finalized_state", new_callable=AsyncMock, side_effect=CheckpointSyncError("HTTP error 404: Not Found"), ):