diff --git a/src/lean_spec/subspecs/genesis/config.py b/src/lean_spec/subspecs/genesis/config.py index 77b846ba2..24f8adf86 100644 --- a/src/lean_spec/subspecs/genesis/config.py +++ b/src/lean_spec/subspecs/genesis/config.py @@ -20,7 +20,7 @@ import yaml from pydantic import Field, field_validator, model_validator -from lean_spec.subspecs.containers import State, Validator +from lean_spec.subspecs.containers import Validator from lean_spec.subspecs.containers.state import Validators from lean_spec.subspecs.containers.validator import ValidatorIndex from lean_spec.types import Bytes52, StrictBaseModel, Uint64 @@ -131,15 +131,6 @@ def to_validators(self) -> Validators: ] ) - def create_state(self) -> State: - """ - Generate the complete genesis state from this configuration. - - Combines genesis time and validator set to create the initial - consensus state. This state becomes slot 0 for the chain. - """ - return State.generate_genesis(self.genesis_time, self.to_validators()) - @classmethod def from_yaml_file(cls, path: Path | str) -> GenesisConfig: """ diff --git a/src/lean_spec/subspecs/koalabear/field.py b/src/lean_spec/subspecs/koalabear/field.py index 4e43f8133..a35b794af 100644 --- a/src/lean_spec/subspecs/koalabear/field.py +++ b/src/lean_spec/subspecs/koalabear/field.py @@ -19,47 +19,6 @@ P_BYTES: Final = (P_BITS + 7) // 8 """The size of a KoalaBear field element in bytes.""" -TWO_ADICITY: Final = 24 -""" -The largest integer n such that 2^n divides (P - 1). - -P - 1 = 2^24 * 127 -""" - -TWO_ADIC_GENERATORS: Final[list[int]] = [ - 0x1, - 0x7F000000, - 0x7E010002, - 0x6832FE4A, - 0x8DBD69C, - 0xA28F031, - 0x5C4A5B99, - 0x29B75A80, - 0x17668B8A, - 0x27AD539B, - 0x334D48C7, - 0x7744959C, - 0x768FC6FA, - 0x303964B2, - 0x3E687D4D, - 0x45A60E61, - 0x6E2F4D7A, - 0x163BD499, - 0x6C4A8A45, - 0x143EF899, - 0x514DDCAD, - 0x484EF19B, - 0x205D63C3, - 0x68E7DD49, - 0x6AC49F88, -] -""" -A pre-computed list of 2^n-th roots of unity. - -The element at index `n` is a generator for -the multiplicative subgroup of order 2^n. -""" - class Fp(SSZType): """ @@ -147,27 +106,6 @@ def __truediv__(self, other: Self) -> Self: """Field division.""" return self * other.inverse() - @classmethod - def two_adic_generator(cls, bits: int) -> Self: - """ - Get a generator for the multiplicative subgroup of order 2^bits. - - This is a direct lookup from a pre-computed list of generators. - - Args: - bits: The order of the subgroup will be 2^bits. - Must be in [0, TWO_ADICITY]. - - Returns: - A generator of the multiplicative subgroup of order 2^bits. - - Raises: - ValueError: If `bits` is outside the valid range. - """ - if not (0 <= bits <= TWO_ADICITY): - raise ValueError(f"bits must be between 0 and {TWO_ADICITY}") - return cls(value=TWO_ADIC_GENERATORS[bits]) - def __eq__(self, other: object) -> bool: """Check equality of two field elements.""" if not isinstance(other, Fp): diff --git a/src/lean_spec/subspecs/networking/enr/enr.py b/src/lean_spec/subspecs/networking/enr/enr.py index 1549bc55f..c27b78fdb 100644 --- a/src/lean_spec/subspecs/networking/enr/enr.py +++ b/src/lean_spec/subspecs/networking/enr/enr.py @@ -109,10 +109,6 @@ def get(self, key: EnrKey) -> bytes | None: """Get value by key, or None if absent.""" return self.pairs.get(key) - def has(self, key: EnrKey) -> bool: - """Check if key is present.""" - return key in self.pairs - @property def identity_scheme(self) -> str | None: """Get identity scheme (should be "v4").""" @@ -220,13 +216,6 @@ def is_valid(self) -> bool: """ return self.identity_scheme == self.SCHEME and self.public_key is not None - def is_compatible_with(self, other: ENR) -> bool: - """Check fork compatibility via eth2 fork digest.""" - self_eth2, other_eth2 = self.eth2_data, other.eth2_data - if self_eth2 is None or other_eth2 is None: - return False - return self_eth2.fork_digest == other_eth2.fork_digest - def _build_content_items(self) -> list[RLPItem]: """ Build the list of content items for RLP encoding. diff --git a/src/lean_spec/subspecs/networking/enr/eth2.py b/src/lean_spec/subspecs/networking/enr/eth2.py index 8d1b0721e..2c0870836 100644 --- a/src/lean_spec/subspecs/networking/enr/eth2.py +++ b/src/lean_spec/subspecs/networking/enr/eth2.py @@ -44,15 +44,6 @@ class Eth2Data(StrictBaseModel): next_fork_epoch: Uint64 """Epoch when next fork activates. FAR_FUTURE_EPOCH if none scheduled.""" - @classmethod - def no_scheduled_fork(cls, current_digest: ForkDigest, current_version: Version) -> "Eth2Data": - """Create Eth2Data indicating no scheduled fork.""" - return cls( - fork_digest=current_digest, - next_fork_version=current_version, - next_fork_epoch=FAR_FUTURE_EPOCH, - ) - class AttestationSubnets(BaseBitvector): """ diff --git a/src/lean_spec/subspecs/networking/gossipsub/behavior.py b/src/lean_spec/subspecs/networking/gossipsub/behavior.py index 995bef051..996fa107f 100644 --- a/src/lean_spec/subspecs/networking/gossipsub/behavior.py +++ b/src/lean_spec/subspecs/networking/gossipsub/behavior.py @@ -59,7 +59,7 @@ import logging import random import time -from collections.abc import Callable, Coroutine +from collections.abc import Coroutine from dataclasses import dataclass, field from itertools import count from typing import ClassVar, Final, cast @@ -229,9 +229,6 @@ class GossipsubBehavior: _heartbeat_task: asyncio.Task[None] | None = None """Background heartbeat task.""" - _message_handler: Callable[[GossipsubMessageEvent], None] | None = None - """Optional callback for received messages.""" - _stop_event: asyncio.Event = field(default_factory=asyncio.Event) """Event to signal stop to the events generator.""" @@ -555,10 +552,6 @@ async def get_next_event( pass return None - def set_message_handler(self, handler: Callable[[GossipsubMessageEvent], None]) -> None: - """Set a callback for received messages.""" - self._message_handler = handler - async def _handle_rpc(self, peer_id: PeerId, rpc: RPC) -> None: """Dispatch an incoming RPC to the appropriate handlers.""" state = self._peers.get(peer_id) @@ -651,9 +644,6 @@ async def _handle_message(self, peer_id: PeerId, msg: Message) -> None: ) await self._event_queue.put(event) - if self._message_handler: - self._message_handler(event) - logger.debug( "Received message %s from %s on topic %s", msg_id.hex()[:8], peer_id, msg.topic ) diff --git a/src/lean_spec/subspecs/networking/gossipsub/mcache.py b/src/lean_spec/subspecs/networking/gossipsub/mcache.py index 983f0143c..e79f8bb97 100644 --- a/src/lean_spec/subspecs/networking/gossipsub/mcache.py +++ b/src/lean_spec/subspecs/networking/gossipsub/mcache.py @@ -226,16 +226,6 @@ def shift(self) -> int: return evicted - def clear(self) -> None: - """Clear all cached messages.""" - self._windows.clear() - self._windows.append(set()) - self._by_id.clear() - - def __len__(self) -> int: - """Return the total number of cached messages.""" - return len(self._by_id) - @dataclass(slots=True) class SeenCache: @@ -316,11 +306,3 @@ def cleanup(self, current_time: float) -> int: del self._timestamps[msg_id] return len(expired) - - def clear(self) -> None: - """Clear all seen entries.""" - self._timestamps.clear() - - def __len__(self) -> int: - """Return the number of seen message IDs.""" - return len(self._timestamps) diff --git a/src/lean_spec/subspecs/networking/gossipsub/rpc.py b/src/lean_spec/subspecs/networking/gossipsub/rpc.py index 5f9b1e8c5..95b8c4288 100644 --- a/src/lean_spec/subspecs/networking/gossipsub/rpc.py +++ b/src/lean_spec/subspecs/networking/gossipsub/rpc.py @@ -614,14 +614,6 @@ def decode(cls, data: bytes) -> RPC: return rpc - def is_empty(self) -> bool: - """Check if this RPC contains no data.""" - return ( - not self.subscriptions - and not self.publish - and (self.control is None or self.control.is_empty()) - ) - @classmethod def subscription(cls, topics: list[TopicId], subscribe: bool = True) -> RPC: """ diff --git a/src/lean_spec/subspecs/networking/transport/identity/keypair.py b/src/lean_spec/subspecs/networking/transport/identity/keypair.py index 3801f7274..77c56e8f7 100644 --- a/src/lean_spec/subspecs/networking/transport/identity/keypair.py +++ b/src/lean_spec/subspecs/networking/transport/identity/keypair.py @@ -16,7 +16,7 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec -from lean_spec.types import Bytes32, Bytes33 +from lean_spec.types import Bytes33 from ..peer_id import KeyType, PeerId, PublicKeyProto @@ -33,20 +33,6 @@ class Secp256k1PublicKey: _key: ec.EllipticCurvePublicKey """The cryptography library's public key object""" - @classmethod - def from_bytes(cls, data: Bytes33) -> Secp256k1PublicKey: - """ - Load from 33-byte compressed SEC1 format. - - Args: - data: 33-byte compressed secp256k1 public key. - - Returns: - Parsed public key. - """ - key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), data) - return cls(_key=key) - def to_bytes(self) -> Bytes33: """ Return the 33-byte compressed SEC1 encoding. @@ -107,34 +93,6 @@ def generate(cls) -> IdentityKeypair: public_key = Secp256k1PublicKey(_key=private_key.public_key()) return cls(private_key=private_key, public_key=public_key) - @classmethod - def from_bytes(cls, data: Bytes32) -> IdentityKeypair: - """ - Load keypair from raw private key bytes. - - Args: - data: 32-byte secp256k1 private key. - - Returns: - Identity keypair. - """ - private_key = ec.derive_private_key( - int.from_bytes(data, "big"), - ec.SECP256K1(), - ) - public_key = Secp256k1PublicKey(_key=private_key.public_key()) - return cls(private_key=private_key, public_key=public_key) - - def private_key_bytes(self) -> Bytes32: - """ - Return the raw 32-byte private key. - - Returns: - 32-byte private key scalar. - """ - private_numbers = self.private_key.private_numbers() - return Bytes32(private_numbers.private_value.to_bytes(32, "big")) - def sign(self, message: bytes) -> bytes: """ Sign a message with ECDSA-SHA256. diff --git a/src/lean_spec/subspecs/networking/transport/peer_id.py b/src/lean_spec/subspecs/networking/transport/peer_id.py index 2a1f48487..49cea09a6 100644 --- a/src/lean_spec/subspecs/networking/transport/peer_id.py +++ b/src/lean_spec/subspecs/networking/transport/peer_id.py @@ -38,7 +38,6 @@ from typing import Final from lean_spec.subspecs.networking import varint -from lean_spec.types import Bytes33 __all__ = [ # Main types @@ -238,37 +237,6 @@ def encode(self) -> bytes: return bytes([self.code, len(self.digest)]) + self.digest - @classmethod - def identity(cls, data: bytes) -> Multihash: - """ - Create an identity multihash (no hashing). - - Args: - data: Data to wrap (must be <= 127 bytes). - - Returns: - Identity multihash. - - Raises: - ValueError: If data exceeds 127 bytes. - """ - if len(data) > 127: - raise ValueError("Identity multihash limited to 127 bytes") - return cls(code=MultihashCode.IDENTITY, digest=data) - - @classmethod - def sha256(cls, data: bytes) -> Multihash: - """ - Create a SHA256 multihash. - - Args: - data: Data to hash. - - Returns: - SHA256 multihash. - """ - return cls(code=MultihashCode.SHA256, digest=hashlib.sha256(data).digest()) - @classmethod def from_data(cls, data: bytes) -> Multihash: """ @@ -286,9 +254,8 @@ def from_data(cls, data: bytes) -> Multihash: Multihash with appropriate hash function. """ if len(data) <= _IDENTITY_THRESHOLD: - return cls.identity(data) - else: - return cls.sha256(data) + return cls(code=MultihashCode.IDENTITY, digest=data) + return cls(code=MultihashCode.SHA256, digest=hashlib.sha256(data).digest()) @dataclass(frozen=True, slots=True) @@ -407,21 +374,3 @@ def from_public_key(cls, public_key: PublicKeyProto) -> PeerId: encoded = public_key.encode() mh = Multihash.from_data(encoded) return cls(multihash=mh.encode()) - - @classmethod - def from_secp256k1(cls, public_key_bytes: Bytes33) -> PeerId: - """ - Derive PeerId from a secp256k1 compressed public key. - - This is the standard method used by ream, zeam, and the Ethereum - libp2p network for peer identification. - - Args: - public_key_bytes: 33-byte compressed secp256k1 public key - (starts with 0x02 or 0x03). - - Returns: - Derived PeerId (starts with "16Uiu2..." for secp256k1). - """ - proto = PublicKeyProto(key_type=KeyType.SECP256K1, key_data=public_key_bytes) - return cls.from_public_key(proto) diff --git a/src/lean_spec/subspecs/sync/block_cache.py b/src/lean_spec/subspecs/sync/block_cache.py index 7c26d0095..fd9d229eb 100644 --- a/src/lean_spec/subspecs/sync/block_cache.py +++ b/src/lean_spec/subspecs/sync/block_cache.py @@ -295,27 +295,6 @@ def unmark_orphan(self, root: Bytes32) -> None: """ self._orphans.discard(root) - def get_orphan_parents(self) -> list[Bytes32]: - """ - Get roots of missing parent blocks for all orphans. - - This is the entry point for backfill. It returns a deduplicated list - of parent roots that need to be fetched to resolve current orphans. - - Deduplication matters because multiple orphan blocks might share the - same missing parent (e.g., two competing blocks at the same slot). - - Returns: - List of parent block roots to fetch via BlocksByRoot requests. - """ - return list( - { - pending.parent_root - for root in self._orphans - if (pending := self._blocks.get(root)) and pending.parent_root not in self._blocks - } - ) - def get_children(self, parent_root: Bytes32) -> list[PendingBlock]: """ Get all cached children of a given parent root. diff --git a/src/lean_spec/subspecs/sync/head_sync.py b/src/lean_spec/subspecs/sync/head_sync.py index 0b397219d..04040192a 100644 --- a/src/lean_spec/subspecs/sync/head_sync.py +++ b/src/lean_spec/subspecs/sync/head_sync.py @@ -385,52 +385,6 @@ async def _cache_and_backfill( descendants_processed=0, ), store - async def process_all_processable(self, store: Store) -> tuple[int, Store]: - """ - Process all blocks in the cache that now have parents in the store. - - Called after backfill completes or store updates to process any - blocks that have become processable. - - Args: - store: Current store. - - Returns: - Tuple of (count of blocks processed, updated store). - """ - processed_count = 0 - - while True: - # Get processable blocks (parents in store). - processable = self.block_cache.get_processable(store) - if not processable: - break - - for pending in processable: - if pending.root in self._processing: - continue - if pending.root in store.blocks: - self.block_cache.remove(pending.root) - continue - - self._processing.add(pending.root) - - try: - try: - store = self.process_block(store, pending.block) - processed_count += 1 - self.block_cache.remove(pending.root) - - except Exception as exc: - # Processing failed. Remove from cache to avoid infinite loop. - logger.debug("Failed to process cached block: %s", exc) - self.block_cache.remove(pending.root) - - finally: - self._processing.discard(pending.root) - - return processed_count, store - def reset(self) -> None: """Clear processing state.""" self._processing.clear() diff --git a/src/lean_spec/subspecs/sync/peer_manager.py b/src/lean_spec/subspecs/sync/peer_manager.py index 66fb59b4e..c4fd9c8e8 100644 --- a/src/lean_spec/subspecs/sync/peer_manager.py +++ b/src/lean_spec/subspecs/sync/peer_manager.py @@ -184,7 +184,3 @@ def on_request_failure(self, peer_id: PeerId) -> None: def get_all_peers(self) -> list[SyncPeer]: """Get all tracked peers.""" return list(self._peers.values()) - - def clear(self) -> None: - """Remove all peers.""" - self._peers.clear() diff --git a/src/lean_spec/subspecs/sync/service.py b/src/lean_spec/subspecs/sync/service.py index 7d0634097..187c130de 100644 --- a/src/lean_spec/subspecs/sync/service.py +++ b/src/lean_spec/subspecs/sync/service.py @@ -36,7 +36,6 @@ from __future__ import annotations -import asyncio import logging from collections.abc import Callable, Coroutine from dataclasses import dataclass, field @@ -225,9 +224,6 @@ class SyncService: _blocks_processed: int = field(default=0) """Counter for processed blocks.""" - _sync_lock: asyncio.Lock = field(default_factory=asyncio.Lock) - """Lock to prevent concurrent sync operations.""" - _pending_attestations: list[SignedAttestation] = field(default_factory=list) """Attestations awaiting block processing. @@ -673,43 +669,6 @@ async def publish_aggregated_attestation( """ await self._publish_agg_fn(signed_attestation) - async def start_sync(self) -> None: - """ - Start or resume synchronization. - - This is the main entry point for initiating sync. It assesses the - current state and begins appropriate sync activities. - """ - # Serialize sync operations to prevent race conditions. - # - # Without this lock, concurrent calls to start_sync could cause - # duplicate state transitions or conflicting sync operations. - async with self._sync_lock: - await self._check_sync_trigger() - - async def process_pending_blocks(self) -> int: - """ - Process all blocks in cache that now have parents. - - Called after backfill completes or when blocks may have become - processable. - - Returns: - Number of blocks processed. - """ - if self._head_sync is None: - raise RuntimeError("HeadSync not initialized") - - # Process blocks in topological order (parents before children). - # - # When backfill fetches missing parents, it may unlock a chain of - # waiting blocks. HeadSync handles the ordering to ensure each block - # is processed only after its parent is in the store. - count, new_store = await self._head_sync.process_all_processable(self.store) - self.store = new_store - - return count - async def _check_sync_trigger(self) -> None: """ Check if sync should be triggered based on current state. diff --git a/src/lean_spec/subspecs/validator/registry.py b/src/lean_spec/subspecs/validator/registry.py index 02b7fe4fe..1bdaf7841 100644 --- a/src/lean_spec/subspecs/validator/registry.py +++ b/src/lean_spec/subspecs/validator/registry.py @@ -316,29 +316,3 @@ def from_yaml( ) return registry - - @classmethod - def from_secret_keys( - cls, keys: dict[ValidatorIndex, tuple[SecretKey, SecretKey]] - ) -> ValidatorRegistry: - """ - Create registry from a dictionary of secret key pairs. - - Convenience method for testing or programmatic key loading. - - Args: - keys: Mapping from validator index to (attestation_secret_key, proposal_secret_key). - - Returns: - Registry populated with provided keys. - """ - registry = cls() - for index, (att_sk, prop_sk) in keys.items(): - registry.add( - ValidatorEntry( - index=index, - attestation_secret_key=att_sk, - proposal_secret_key=prop_sk, - ) - ) - return registry diff --git a/tests/lean_spec/subspecs/genesis/test_config.py b/tests/lean_spec/subspecs/genesis/test_config.py index 69d07dc8b..d717027b1 100644 --- a/tests/lean_spec/subspecs/genesis/test_config.py +++ b/tests/lean_spec/subspecs/genesis/test_config.py @@ -8,7 +8,6 @@ import yaml from pydantic import ValidationError -from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.genesis import GenesisConfig from lean_spec.types import Bytes52, SSZValueError, Uint64 @@ -120,20 +119,6 @@ def test_empty_validators_list(self) -> None: assert len(validators.data) == 0 -class TestGenesisConfigState: - """Tests for state creation.""" - - def test_create_state_returns_valid_genesis(self) -> None: - """State has correct genesis time and validators.""" - config = GenesisConfig.from_yaml(SAMPLE_YAML) - state = config.create_state() - - # Genesis time is stored in the state's config. - assert state.config.genesis_time == config.genesis_time - assert state.slot == Slot(0) - assert len(state.validators.data) == 3 - - class TestGenesisConfigValidation: """Tests for validation errors.""" diff --git a/tests/lean_spec/subspecs/koalabear/test_field.py b/tests/lean_spec/subspecs/koalabear/test_field.py index d4b281b93..70f43bc37 100644 --- a/tests/lean_spec/subspecs/koalabear/test_field.py +++ b/tests/lean_spec/subspecs/koalabear/test_field.py @@ -7,20 +7,12 @@ import pytest -from lean_spec.subspecs.koalabear.field import ( - TWO_ADIC_GENERATORS, - TWO_ADICITY, - Fp, - P, -) +from lean_spec.subspecs.koalabear.field import Fp, P def test_constants() -> None: """Verify field constants.""" assert P == 2**31 - 2**24 + 1 - assert (P - 1) % (2**TWO_ADICITY) == 0 - assert (P - 1) % (2 ** (TWO_ADICITY + 1)) != 0 - assert len(TWO_ADIC_GENERATORS) == TWO_ADICITY + 1 def test_base_field_arithmetic() -> None: @@ -49,32 +41,6 @@ def test_base_field_arithmetic() -> None: Fp(value=0).inverse() -def test_two_adicity() -> None: - """Test the properties and error handling of the two-adic generators.""" - bits = 4 # 2^4 = 16 - gen = Fp.two_adic_generator(bits) - assert gen == Fp(value=TWO_ADIC_GENERATORS[bits]) - - # Check that the generator has the correct order - assert gen ** (2**bits) == Fp(value=1) - assert gen ** (2 ** (bits - 1)) != Fp(value=1) - - # Check relationship between generators: g_n^2 should equal g_{n-1} - gen_n = Fp.two_adic_generator(TWO_ADICITY) - gen_n_minus_1 = Fp.two_adic_generator(TWO_ADICITY - 1) - assert gen_n**2 == gen_n_minus_1 - - # The largest order generator should square to -1 - assert gen_n ** (2 ** (TWO_ADICITY - 1)) == Fp(value=-1) - - # Test error handling for out-of-bounds input - with pytest.raises(ValueError, match=f"bits must be between 0 and {TWO_ADICITY}"): - Fp.two_adic_generator(TWO_ADICITY + 1) - - with pytest.raises(ValueError, match=f"bits must be between 0 and {TWO_ADICITY}"): - Fp.two_adic_generator(-1) - - def test_bytes_protocol() -> None: """Test serialization using Python's bytes protocol.""" # Test basic serialization diff --git a/tests/lean_spec/subspecs/networking/enr/test_enr.py b/tests/lean_spec/subspecs/networking/enr/test_enr.py index 65ad5053d..0f9135985 100644 --- a/tests/lean_spec/subspecs/networking/enr/test_enr.py +++ b/tests/lean_spec/subspecs/networking/enr/test_enr.py @@ -609,27 +609,6 @@ def test_get_missing_key(self) -> None: ) assert enr.get(keys.IP) is None - def test_has_existing_key(self) -> None: - """has() returns True for existing key.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.IP: b"\x7f\x00\x00\x01"}, - ) - assert enr.has(keys.ID) - assert enr.has(keys.IP) - - def test_has_missing_key(self) -> None: - """has() returns False for missing key.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4"}, - ) - assert not enr.has(keys.IP) - assert not enr.has(keys.UDP) - assert not enr.has(keys.ETH2) - class TestEdgeCases: """Tests for edge cases and boundary conditions.""" @@ -795,167 +774,6 @@ def test_eth2_data_returns_none_for_short_data(self) -> None: assert enr.eth2_data is None -class TestAttestationSubnetsProperty: - """Tests for attestation_subnets property parsing.""" - - def test_attestation_subnets_parses_from_enr(self) -> None: - """attestation_subnets property parses 8-byte attnets key.""" - # All bits set (64 bits = 8 bytes of 0xFF) - attnets_bytes = b"\xff" * 8 - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.ATTNETS: attnets_bytes}, - ) - - attnets = enr.attestation_subnets - assert attnets is not None - assert attnets.subscription_count() == 64 - - def test_attestation_subnets_returns_none_when_missing(self) -> None: - """attestation_subnets returns None when attnets key is absent.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4"}, - ) - assert enr.attestation_subnets is None - - def test_attestation_subnets_returns_none_for_wrong_length(self) -> None: - """attestation_subnets returns None when attnets key is not 8 bytes.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.ATTNETS: b"\xff\xff\xff\xff"}, # Only 4 bytes - ) - assert enr.attestation_subnets is None - - -class TestSyncCommitteeSubnetsProperty: - """Tests for sync_committee_subnets property parsing.""" - - def test_sync_committee_subnets_parses_from_enr(self) -> None: - """sync_committee_subnets property parses 1-byte syncnets key.""" - # All 4 bits set (lower nibble of 0x0F) - syncnets_bytes = b"\x0f" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.SYNCNETS: syncnets_bytes}, - ) - - syncnets = enr.sync_committee_subnets - assert syncnets is not None - for i in range(4): - assert syncnets.is_subscribed(i) - - def test_sync_committee_subnets_returns_none_when_missing(self) -> None: - """sync_committee_subnets returns None when syncnets key is absent.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4"}, - ) - assert enr.sync_committee_subnets is None - - def test_sync_committee_subnets_returns_none_for_wrong_length(self) -> None: - """sync_committee_subnets returns None when syncnets key is not 1 byte.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.SYNCNETS: b"\x0f\x00"}, # 2 bytes - ) - assert enr.sync_committee_subnets is None - - -class TestForkCompatibility: - """Tests for is_compatible_with() method.""" - - def test_compatible_with_same_fork_digest(self) -> None: - """ENRs with same fork digest are compatible.""" - eth2_bytes = b"\x12\x34\x56\x78" + b"\x02\x00\x00\x00" + b"\x00" * 8 - - enr1 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.ETH2: eth2_bytes}, - ) - enr2 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(2), - pairs={keys.ID: b"v4", keys.ETH2: eth2_bytes}, - ) - - assert enr1.is_compatible_with(enr2) - - def test_incompatible_with_different_fork_digest(self) -> None: - """ENRs with different fork digests are incompatible.""" - eth2_bytes1 = b"\x12\x34\x56\x78" + b"\x02\x00\x00\x00" + b"\x00" * 8 - eth2_bytes2 = b"\xab\xcd\xef\x01" + b"\x02\x00\x00\x00" + b"\x00" * 8 - - enr1 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.ETH2: eth2_bytes1}, - ) - enr2 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(2), - pairs={keys.ID: b"v4", keys.ETH2: eth2_bytes2}, - ) - - assert not enr1.is_compatible_with(enr2) - - def test_incompatible_when_self_missing_eth2(self) -> None: - """ENR is incompatible when self lacks eth2 key.""" - eth2_bytes = b"\x12\x34\x56\x78" + b"\x02\x00\x00\x00" + b"\x00" * 8 - - enr1 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4"}, # No eth2 - ) - enr2 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(2), - pairs={keys.ID: b"v4", keys.ETH2: eth2_bytes}, - ) - - assert not enr1.is_compatible_with(enr2) - - def test_incompatible_when_other_missing_eth2(self) -> None: - """ENR is incompatible when other lacks eth2 key.""" - eth2_bytes = b"\x12\x34\x56\x78" + b"\x02\x00\x00\x00" + b"\x00" * 8 - - enr1 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4", keys.ETH2: eth2_bytes}, - ) - enr2 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(2), - pairs={keys.ID: b"v4"}, # No eth2 - ) - - assert not enr1.is_compatible_with(enr2) - - def test_incompatible_when_both_missing_eth2(self) -> None: - """ENRs are incompatible when both lack eth2 key.""" - enr1 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4"}, - ) - enr2 = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(2), - pairs={keys.ID: b"v4"}, - ) - - assert not enr1.is_compatible_with(enr2) - - class TestMaxSizeEnforcement: """Tests for MAX_SIZE (300 bytes) enforcement.""" @@ -1305,42 +1123,3 @@ def test_node_id_none_without_public_key(self) -> None: ) assert enr.compute_node_id() is None - - -class TestIPv6Ports: - """Tests for udp6_port property.""" - - def test_udp6_port_extracts_correctly(self) -> None: - """udp6_port extracts IPv6-specific UDP port.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={ - keys.ID: b"v4", - keys.UDP6: (30304).to_bytes(2, "big"), - }, - ) - assert enr.udp6_port == Port(30304) - - def test_udp6_port_returns_none_when_missing(self) -> None: - """udp6_port returns None when udp6 key is absent.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={keys.ID: b"v4"}, - ) - assert enr.udp6_port is None - - def test_ipv6_udp_port_independent_of_ipv4(self) -> None: - """IPv6 UDP port is independent from IPv4 UDP port.""" - enr = ENR( - signature=Bytes64(b"\x00" * 64), - seq=SeqNumber(1), - pairs={ - keys.ID: b"v4", - keys.UDP: (30303).to_bytes(2, "big"), - keys.UDP6: (30304).to_bytes(2, "big"), - }, - ) - assert enr.udp_port == Port(30303) - assert enr.udp6_port == Port(30304) diff --git a/tests/lean_spec/subspecs/networking/enr/test_eth2.py b/tests/lean_spec/subspecs/networking/enr/test_eth2.py index 28d945eda..5992fe7bd 100644 --- a/tests/lean_spec/subspecs/networking/enr/test_eth2.py +++ b/tests/lean_spec/subspecs/networking/enr/test_eth2.py @@ -27,16 +27,6 @@ def test_create_eth2_data(self) -> None: assert data.fork_digest == ForkDigest(b"\x12\x34\x56\x78") assert data.next_fork_epoch == Uint64(194048) - def test_no_scheduled_fork_factory(self) -> None: - """no_scheduled_fork factory creates correct data.""" - digest = ForkDigest(b"\xab\xcd\xef\x01") - version = Version(b"\x01\x00\x00\x00") - data = Eth2Data.no_scheduled_fork(digest, version) - - assert data.fork_digest == digest - assert data.next_fork_version == version - assert data.next_fork_epoch == FAR_FUTURE_EPOCH - def test_eth2_data_immutable(self) -> None: """Eth2Data is immutable (frozen).""" data = Eth2Data( diff --git a/tests/lean_spec/subspecs/networking/gossipsub/test_cache_edge_cases.py b/tests/lean_spec/subspecs/networking/gossipsub/test_cache_edge_cases.py index 71035db0d..cd345e692 100644 --- a/tests/lean_spec/subspecs/networking/gossipsub/test_cache_edge_cases.py +++ b/tests/lean_spec/subspecs/networking/gossipsub/test_cache_edge_cases.py @@ -21,7 +21,6 @@ def test_shift_when_not_full(self) -> None: # Only 1 window used out of 6; shift should evict nothing. evicted = cache.shift() assert evicted == 0 - assert len(cache) == 1 assert cache.has(msg.id) def test_shift_evicts_oldest_window(self) -> None: @@ -52,38 +51,12 @@ def test_shift_returns_correct_eviction_count(self) -> None: # One shift: still within capacity (2 windows). evicted = cache.shift() assert evicted == 0 - assert len(cache) == 3 + assert all(cache.has(m.id) for m in msgs) # Second shift: oldest window (with 3 msgs) is evicted. evicted = cache.shift() assert evicted == 3 - assert len(cache) == 0 - - -class TestMessageCacheClear: - """Tests for MessageCache.clear().""" - - def test_clear_empties_all(self) -> None: - """clear() removes all messages and resets windows.""" - cache = MessageCache() - for i in range(5): - msg = GossipsubMessage(topic=b"t", raw_data=f"d{i}".encode()) - cache.put(TopicId("t"), msg) - - assert len(cache) == 5 - cache.clear() - assert len(cache) == 0 - - def test_clear_allows_reuse(self) -> None: - """After clear(), new messages can be added normally.""" - cache = MessageCache() - old_msg = GossipsubMessage(topic=b"t", raw_data=b"old") - cache.put(TopicId("t"), old_msg) - cache.clear() - - new_msg = GossipsubMessage(topic=b"t", raw_data=b"new") - assert cache.put(TopicId("t"), new_msg) is True - assert len(cache) == 1 + assert not any(cache.has(m.id) for m in msgs) class TestMessageCacheGetGossipIds: @@ -167,7 +140,7 @@ def test_put_duplicate_returns_false(self) -> None: assert cache.put(TopicId("t"), msg) is True assert cache.put(TopicId("t"), msg) is False - assert len(cache) == 1 + assert cache.has(msg.id) def test_has_method(self) -> None: """The has() method works for message IDs.""" @@ -214,21 +187,12 @@ def test_cleanup_no_expired(self) -> None: """cleanup() with no expired entries removes nothing.""" seen = SeenCache(ttl_seconds=120) now = time.time() - seen.add(MessageId(b"12345678901234567890"), Timestamp(now)) + msg_id = MessageId(b"12345678901234567890") + seen.add(msg_id, Timestamp(now)) removed = seen.cleanup(now) assert removed == 0 - assert len(seen) == 1 - - def test_clear_empties_all(self) -> None: - """clear() removes all entries.""" - seen = SeenCache() - for i in range(5): - seen.add(MessageId(f"x{i:019d}".encode()), Timestamp(time.time())) - - assert len(seen) == 5 - seen.clear() - assert len(seen) == 0 + assert seen.has(msg_id) def test_has_method(self) -> None: """The has() method works for seen message IDs.""" diff --git a/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py b/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py index 4b176739a..8f88312cf 100644 --- a/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py +++ b/tests/lean_spec/subspecs/networking/gossipsub/test_gossipsub.py @@ -471,14 +471,6 @@ def test_rpc_full_message(self) -> None: ) assert RPC.decode(rpc.encode()) == rpc - def test_rpc_empty_check(self) -> None: - """Test RPC is_empty method.""" - empty_rpc = RPC() - assert empty_rpc.is_empty() - - non_empty = RPC(subscriptions=[SubOpts(subscribe=True, topic_id=TopicId("/topic"))]) - assert not non_empty.is_empty() - def test_rpc_helper_functions(self) -> None: """Test RPC creation helper functions.""" assert RPC.subscription([TopicId("/topic1"), TopicId("/topic2")], subscribe=True) == RPC( diff --git a/tests/lean_spec/subspecs/networking/gossipsub/test_handlers.py b/tests/lean_spec/subspecs/networking/gossipsub/test_handlers.py index 70a6f01bf..8f93341c4 100644 --- a/tests/lean_spec/subspecs/networking/gossipsub/test_handlers.py +++ b/tests/lean_spec/subspecs/networking/gossipsub/test_handlers.py @@ -402,27 +402,6 @@ async def test_message_event_emitted(self) -> None: message_id=GossipsubMessage.compute_id(b"topic", b"payload"), ) - @pytest.mark.asyncio - async def test_message_callback_invoked(self) -> None: - """Message handler callback is invoked.""" - behavior, _ = make_behavior() - peer_id = add_peer(behavior, "peer1") - - received: list[GossipsubMessageEvent] = [] - behavior.set_message_handler(received.append) - - msg = Message(topic=TopicId("topic"), data=b"data") - await behavior._handle_message(peer_id, msg) - - assert received == [ - GossipsubMessageEvent( - peer_id=peer_id, - topic=TopicId("topic"), - data=b"data", - message_id=GossipsubMessage.compute_id(b"topic", b"data"), - ) - ] - @pytest.mark.asyncio async def test_empty_topic_ignored(self) -> None: """Message with empty topic is silently dropped.""" diff --git a/tests/lean_spec/subspecs/networking/gossipsub/test_heartbeat.py b/tests/lean_spec/subspecs/networking/gossipsub/test_heartbeat.py index a67863676..d70f4e064 100644 --- a/tests/lean_spec/subspecs/networking/gossipsub/test_heartbeat.py +++ b/tests/lean_spec/subspecs/networking/gossipsub/test_heartbeat.py @@ -239,15 +239,14 @@ async def test_shifts_message_cache(self) -> None: msg = GossipsubMessage(topic=b"topic", raw_data=b"data") behavior.message_cache.put(TopicId("topic"), msg) - initial_len = len(behavior.message_cache) - assert initial_len == 1 + assert behavior.message_cache.has(msg.id) # Run heartbeat several times to shift through all windows for _ in range(7): await behavior._heartbeat() # After enough shifts, old messages should be evicted - assert len(behavior.message_cache) == 0 + assert not behavior.message_cache.has(msg.id) @pytest.mark.asyncio async def test_cleans_seen_cache(self) -> None: diff --git a/tests/lean_spec/subspecs/networking/transport/identity/test_keypair.py b/tests/lean_spec/subspecs/networking/transport/identity/test_keypair.py index 5e3fb96c8..4676743b0 100644 --- a/tests/lean_spec/subspecs/networking/transport/identity/test_keypair.py +++ b/tests/lean_spec/subspecs/networking/transport/identity/test_keypair.py @@ -1,26 +1,12 @@ """Tests for secp256k1 identity keypair.""" -import pytest - -from lean_spec.subspecs.networking.transport.identity import ( - IdentityKeypair, - Secp256k1PublicKey, -) +from lean_spec.subspecs.networking.transport.identity import IdentityKeypair from lean_spec.subspecs.networking.transport.peer_id import KeyType -from lean_spec.types import Bytes33 class TestSecp256k1PublicKey: """Tests for Secp256k1PublicKey class.""" - def test_from_bytes_roundtrip(self) -> None: - """Public key can be loaded from raw bytes and re-serialized.""" - keypair = IdentityKeypair.generate() - raw = keypair.public_key.to_bytes() - - restored = Secp256k1PublicKey.from_bytes(raw) - assert restored.to_bytes() == raw - def test_verify_valid_signature(self) -> None: """Valid signature passes verification.""" keypair = IdentityKeypair.generate() @@ -44,11 +30,6 @@ def test_verify_wrong_key(self) -> None: assert not keypair2.public_key.verify(b"test message", signature) - def test_from_bytes_invalid(self) -> None: - """Invalid bytes raise an error.""" - with pytest.raises(ValueError): - Secp256k1PublicKey.from_bytes(Bytes33(bytes(33))) - class TestIdentityKeypair: """Tests for IdentityKeypair class.""" @@ -61,23 +42,12 @@ def test_generate(self) -> None: assert len(public_key) == 33 assert public_key[0] in (0x02, 0x03) - private_key = keypair.private_key_bytes() - assert len(private_key) == 32 - def test_generate_unique(self) -> None: """Each generated keypair is unique.""" keypair1 = IdentityKeypair.generate() keypair2 = IdentityKeypair.generate() assert keypair1.public_key.to_bytes() != keypair2.public_key.to_bytes() - assert keypair1.private_key_bytes() != keypair2.private_key_bytes() - - def test_from_bytes_roundtrip(self) -> None: - """Keypair can be loaded from raw bytes.""" - original = IdentityKeypair.generate() - restored = IdentityKeypair.from_bytes(original.private_key_bytes()) - assert restored.public_key.to_bytes() == original.public_key.to_bytes() - assert restored.private_key_bytes() == original.private_key_bytes() def test_sign_and_verify(self) -> None: """Signatures can be verified.""" diff --git a/tests/lean_spec/subspecs/networking/transport/test_peer_id.py b/tests/lean_spec/subspecs/networking/transport/test_peer_id.py index 21a325aea..57dafbc63 100644 --- a/tests/lean_spec/subspecs/networking/transport/test_peer_id.py +++ b/tests/lean_spec/subspecs/networking/transport/test_peer_id.py @@ -23,7 +23,6 @@ PeerId, PublicKeyProto, ) -from lean_spec.types import Bytes33 # Protobuf tag constants for test assertions _PROTOBUF_TAG_TYPE = 0x08 # (1 << 3) | 0 = field 1, varint @@ -98,31 +97,20 @@ class TestMultihash: """Tests for multihash functions.""" def test_identity_multihash_format(self) -> None: - """Identity multihash: [0x00][length][data].""" + """Small data uses identity multihash: [0x00][length][data].""" data = b"test" - mh = Multihash.identity(data) + mh = Multihash.from_data(data) result = mh.encode() assert result[0] == MultihashCode.IDENTITY # 0x00 assert result[1] == len(data) # 4 assert result[2:] == data - def test_identity_multihash_max_length(self) -> None: - """Identity multihash limited to 127 bytes.""" - # 127 bytes should work - data = bytes(127) - mh = Multihash.identity(data) - result = mh.encode() - assert len(result) == 2 + 127 - - # 128 bytes should fail - with pytest.raises(ValueError, match="127 bytes"): - Multihash.identity(bytes(128)) - def test_sha256_multihash_format(self) -> None: - """SHA256 multihash: [0x12][0x20][32-byte hash].""" - data = b"test data" - mh = Multihash.sha256(data) + """Large data uses SHA256 multihash: [0x12][0x20][32-byte hash].""" + # 50 bytes > 42-byte identity threshold → SHA256 path + data = bytes(50) + mh = Multihash.from_data(data) result = mh.encode() assert result[0] == MultihashCode.SHA256 # 0x12 @@ -130,10 +118,10 @@ def test_sha256_multihash_format(self) -> None: assert len(result) == 2 + 32 def test_sha256_multihash_deterministic(self) -> None: - """Same input produces same multihash.""" - data = b"deterministic test" - result1 = Multihash.sha256(data).encode() - result2 = Multihash.sha256(data).encode() + """Same large input produces same multihash.""" + data = bytes(50) + result1 = Multihash.from_data(data).encode() + result2 = Multihash.from_data(data).encode() assert result1 == result2 @@ -245,7 +233,7 @@ class TestDerivePeerId: def test_derive_from_secp256k1(self) -> None: """Derive PeerId from secp256k1 public key.""" keypair = IdentityKeypair.generate() - peer_id = PeerId.from_secp256k1(keypair.public_key.to_bytes()) + peer_id = keypair.to_peer_id() # Result should be a valid Base58 string peer_id_str = str(peer_id) @@ -257,10 +245,9 @@ def test_derive_from_secp256k1(self) -> None: def test_derive_deterministic(self) -> None: """Same key always produces same PeerId.""" keypair = IdentityKeypair.generate() - public_key_bytes = keypair.public_key.to_bytes() - peer_id1 = PeerId.from_secp256k1(public_key_bytes) - peer_id2 = PeerId.from_secp256k1(public_key_bytes) + peer_id1 = keypair.to_peer_id() + peer_id2 = keypair.to_peer_id() assert str(peer_id1) == str(peer_id2) @@ -269,8 +256,8 @@ def test_different_keys_different_peerids(self) -> None: keypair1 = IdentityKeypair.generate() keypair2 = IdentityKeypair.generate() - peer_id1 = PeerId.from_secp256k1(keypair1.public_key.to_bytes()) - peer_id2 = PeerId.from_secp256k1(keypair2.public_key.to_bytes()) + peer_id1 = keypair1.to_peer_id() + peer_id2 = keypair2.to_peer_id() assert str(peer_id1) != str(peer_id2) @@ -282,7 +269,7 @@ def test_peer_id_uses_identity_hash_for_small_keys(self) -> None: """Small encoded keys use identity multihash.""" # secp256k1 key: 33 bytes, encoded is 37 bytes (< 42) keypair = IdentityKeypair.generate() - peer_id = PeerId.from_secp256k1(keypair.public_key.to_bytes()) + peer_id = keypair.to_peer_id() # Decode to verify structure decoded = Base58.decode(str(peer_id)) @@ -350,7 +337,7 @@ def test_ed25519_from_spec_test_vector(self) -> None: assert our_encoded == spec_encoded, "Our encoding must match spec" # Compute PeerId (36 bytes <= 42, uses identity multihash) - multihash = Multihash.identity(spec_encoded).encode() + multihash = Multihash.from_data(spec_encoded).encode() peer_id = Base58.encode(multihash) # Expected PeerId computed from spec test vector @@ -388,7 +375,7 @@ def test_secp256k1_from_spec_test_vector(self) -> None: assert our_encoded == spec_encoded, "Our encoding must match spec" # Compute PeerId (37 bytes <= 42, uses identity multihash) - multihash = Multihash.identity(spec_encoded).encode() + multihash = Multihash.from_data(spec_encoded).encode() peer_id = Base58.encode(multihash) # Expected PeerId computed from spec test vector @@ -432,7 +419,7 @@ def test_ecdsa_from_spec_test_vector(self) -> None: assert our_encoded == spec_encoded, "Our encoding must match spec" # Compute PeerId (95 bytes > 42, uses SHA256 multihash) - multihash = Multihash.sha256(spec_encoded).encode() + multihash = Multihash.from_data(spec_encoded).encode() peer_id = Base58.encode(multihash) # Expected PeerId computed from spec test vector @@ -450,7 +437,7 @@ def test_ed25519_peer_id_prefix(self) -> None: key_data = bytes(32) # All zeros proto = PublicKeyProto(key_type=KeyType.ED25519, key_data=key_data) encoded = proto.encode() - multihash = Multihash.identity(encoded).encode() + multihash = Multihash.from_data(encoded).encode() peer_id = Base58.encode(multihash) assert peer_id.startswith("12D3KooW") @@ -463,7 +450,7 @@ def test_secp256k1_peer_id_prefix(self) -> None: key_data = bytes([0x02] + [0] * 32) # Compressed format proto = PublicKeyProto(key_type=KeyType.SECP256K1, key_data=key_data) encoded = proto.encode() - multihash = Multihash.identity(encoded).encode() + multihash = Multihash.from_data(encoded).encode() peer_id = Base58.encode(multihash) # All secp256k1 PeerIds start with "16Uiu2" (from 00 25 08 02) @@ -475,10 +462,11 @@ def test_known_secp256k1_peer_id(self) -> None: This matches the libp2p spec test vector. """ # From spec: 08021221037777e994e452c21604f91de093ce415f5432f701dd8cd1a7a6fea0e630bfca99 - key_data = Bytes33( - bytes.fromhex("037777e994e452c21604f91de093ce415f5432f701dd8cd1a7a6fea0e630bfca99") + key_data = bytes.fromhex( + "037777e994e452c21604f91de093ce415f5432f701dd8cd1a7a6fea0e630bfca99" ) - peer_id = PeerId.from_secp256k1(key_data) + proto = PublicKeyProto(key_type=KeyType.SECP256K1, key_data=key_data) + peer_id = PeerId.from_public_key(proto) # Expected PeerId from spec test vector expected = "16Uiu2HAmLhLvBoYaoZfaMUKuibM6ac163GwKY74c5kiSLg5KvLpY" @@ -491,7 +479,7 @@ def test_known_secp256k1_peer_id(self) -> None: def test_peer_id_length_reasonable(self) -> None: """PeerId length is reasonable (not too long).""" keypair = IdentityKeypair.generate() - peer_id = PeerId.from_secp256k1(keypair.public_key.to_bytes()) + peer_id = keypair.to_peer_id() # Identity-hash PeerId should be around 52-60 characters # (Base58 encoding of ~39 bytes: 2 multihash header + 37 encoded key) @@ -504,28 +492,19 @@ class TestIntegration: def test_derive_from_generated_keypair(self) -> None: """Derive PeerId from freshly generated keypair.""" keypair = IdentityKeypair.generate() - peer_id = PeerId.from_secp256k1(keypair.public_key.to_bytes()) + peer_id = keypair.to_peer_id() assert len(str(peer_id)) > 0 # Verify structure decoded = Base58.decode(str(peer_id)) assert decoded[0] == MultihashCode.IDENTITY - def test_keypair_to_peer_id_matches(self) -> None: - """IdentityKeypair.to_peer_id() matches from_secp256k1().""" - keypair = IdentityKeypair.generate() - - peer_id1 = keypair.to_peer_id() - peer_id2 = PeerId.from_secp256k1(keypair.public_key.to_bytes()) - - assert str(peer_id1) == str(peer_id2) - def test_multiple_keypairs_unique_peerids(self) -> None: """Each keypair produces unique PeerId.""" peer_ids = set() for _ in range(10): keypair = IdentityKeypair.generate() - peer_id = PeerId.from_secp256k1(keypair.public_key.to_bytes()) + peer_id = keypair.to_peer_id() peer_ids.add(str(peer_id)) # All 10 should be unique diff --git a/tests/lean_spec/subspecs/sync/test_backfill_sync.py b/tests/lean_spec/subspecs/sync/test_backfill_sync.py index aec7fcb66..fe0a172c9 100644 --- a/tests/lean_spec/subspecs/sync/test_backfill_sync.py +++ b/tests/lean_spec/subspecs/sync/test_backfill_sync.py @@ -227,84 +227,6 @@ async def test_network_failure_handled_gracefully( assert backfill_system._pending == set() -class TestOrphanHandling: - """Tests for orphan block management during backfill.""" - - async def test_orphan_parents_fetched_via_fill_missing( - self, - backfill_system: BackfillSync, - network: MockNetworkRequester, - peer_id: PeerId, - ) -> None: - """Fetching orphan parents via get_orphan_parents + fill_missing resolves orphans.""" - parent = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=Bytes32.zero(), - state_root=Bytes32.zero(), - ) - parent_root = network.add_block(parent) - - child = make_signed_block( - slot=Slot(2), - proposer_index=ValidatorIndex(0), - parent_root=parent_root, - state_root=Bytes32(b"\x01" * 32), - ) - child_pending = backfill_system.block_cache.add(child, peer_id) - backfill_system.block_cache.mark_orphan(child_pending.root) - - assert backfill_system.block_cache.orphan_count == 1 - - orphan_parents = backfill_system.block_cache.get_orphan_parents() - await backfill_system.fill_missing(orphan_parents) - - assert parent_root in backfill_system.block_cache - - async def test_shared_parent_deduplicated( - self, - backfill_system: BackfillSync, - network: MockNetworkRequester, - peer_id: PeerId, - ) -> None: - """Multiple orphans with same parent only trigger one request for that parent.""" - parent = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=Bytes32.zero(), - state_root=Bytes32.zero(), - ) - parent_root = network.add_block(parent) - - child1 = make_signed_block( - slot=Slot(2), - proposer_index=ValidatorIndex(0), - parent_root=parent_root, - state_root=Bytes32(b"\x01" * 32), - ) - child2 = make_signed_block( - slot=Slot(2), - proposer_index=ValidatorIndex(1), - parent_root=parent_root, - state_root=Bytes32(b"\x02" * 32), - ) - - pending1 = backfill_system.block_cache.add(child1, peer_id) - pending2 = backfill_system.block_cache.add(child2, peer_id) - backfill_system.block_cache.mark_orphan(pending1.root) - backfill_system.block_cache.mark_orphan(pending2.root) - - orphan_parents = backfill_system.block_cache.get_orphan_parents() - await backfill_system.fill_missing(orphan_parents) - - all_requested_roots = [root for _, roots in network.request_log for root in roots] - assert all_requested_roots.count(parent_root) == 1 - - assert parent_root in network.request_log[0][1] - - assert parent_root in backfill_system.block_cache - - class TestRequestTracking: """Tests for request in-flight tracking.""" diff --git a/tests/lean_spec/subspecs/sync/test_block_cache.py b/tests/lean_spec/subspecs/sync/test_block_cache.py index 59504e961..031b6ffaa 100644 --- a/tests/lean_spec/subspecs/sync/test_block_cache.py +++ b/tests/lean_spec/subspecs/sync/test_block_cache.py @@ -340,81 +340,6 @@ def test_remove_clears_orphan_status(self, peer_id: PeerId) -> None: assert cache.orphan_count == 0 - def test_get_orphan_parents(self, peer_id: PeerId) -> None: - """get_orphan_parents returns missing parent roots for orphans.""" - cache = BlockCache() - parent_root = Bytes32(b"\x01" * 32) - block = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=parent_root, - state_root=Bytes32.zero(), - ) - - pending = cache.add(block, peer_id) - cache.mark_orphan(pending.root) - - orphan_parents = cache.get_orphan_parents() - - assert orphan_parents == [parent_root] - - def test_get_orphan_parents_deduplicates(self, peer_id: PeerId) -> None: - """get_orphan_parents deduplicates when multiple orphans share a parent.""" - cache = BlockCache() - common_parent = Bytes32(b"\x01" * 32) - - # Two orphan blocks with the same missing parent - block1 = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=common_parent, - state_root=Bytes32.zero(), - ) - block2 = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(1), - parent_root=common_parent, - state_root=Bytes32(b"\x02" * 32), - ) - - pending1 = cache.add(block1, peer_id) - pending2 = cache.add(block2, peer_id) - cache.mark_orphan(pending1.root) - cache.mark_orphan(pending2.root) - - orphan_parents = cache.get_orphan_parents() - - # Should return the common parent only once - assert orphan_parents == [common_parent] - - def test_get_orphan_parents_excludes_cached_parents(self, peer_id: PeerId) -> None: - """get_orphan_parents excludes parents that are already in the cache.""" - cache = BlockCache() - - # Add a parent block - parent_block = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=Bytes32.zero(), - state_root=Bytes32.zero(), - ) - parent_pending = cache.add(parent_block, peer_id) - - # Add child block that references the cached parent - child_block = make_signed_block( - slot=Slot(2), - proposer_index=ValidatorIndex(0), - parent_root=parent_pending.root, - state_root=Bytes32.zero(), - ) - child_pending = cache.add(child_block, peer_id) - cache.mark_orphan(child_pending.root) - - orphan_parents = cache.get_orphan_parents() - - # Parent is in cache, so should not be returned - assert orphan_parents == [] - class TestBlockCacheParentChildIndex: """Tests for parent-to-children index in BlockCache.""" diff --git a/tests/lean_spec/subspecs/sync/test_head_sync.py b/tests/lean_spec/subspecs/sync/test_head_sync.py index 9a70b921e..52f94578f 100644 --- a/tests/lean_spec/subspecs/sync/test_head_sync.py +++ b/tests/lean_spec/subspecs/sync/test_head_sync.py @@ -283,95 +283,6 @@ def track_processing(s: Any, block: SignedBlock) -> Any: assert processing_order == [1, 2, 3, 4] -class TestProcessAllProcessable: - """Tests for batch processing of processable blocks.""" - - async def test_processes_all_blocks_with_known_parents( - self, - genesis_block, - peer_id: PeerId, - ) -> None: - """All blocks whose parents are in store are processed.""" - genesis_root = hash_tree_root(genesis_block) - store = cast(Store, MockForkchoiceStore()) - store.blocks[genesis_root] = genesis_block - block_cache = BlockCache() - - # Create two independent blocks with genesis as parent - block1 = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=genesis_root, - state_root=Bytes32(b"\x01" * 32), - ) - block2 = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(1), - parent_root=genesis_root, - state_root=Bytes32(b"\x02" * 32), - ) - - block_cache.add(block1, peer_id) - block_cache.add(block2, peer_id) - - processed_count = 0 - - def count_processing(s: Any, block: SignedBlock) -> Any: - nonlocal processed_count - processed_count += 1 - root = hash_tree_root(block.block) - new_store = MockForkchoiceStore() - new_store.blocks = dict(s.blocks) - new_store.blocks[root] = object() - return new_store - - head_sync = HeadSync( - block_cache=block_cache, - backfill=_null_backfill(), - process_block=count_processing, - ) - - count, _ = await head_sync.process_all_processable(store) - - assert count == 2 - assert processed_count == 2 - assert len(block_cache) == 0 # Both removed - - async def test_processing_failure_removes_block_from_cache( - self, - genesis_block, - peer_id: PeerId, - ) -> None: - """Failed blocks are removed to prevent infinite retry loops.""" - genesis_root = hash_tree_root(genesis_block) - store = cast(Store, MockForkchoiceStore()) - store.blocks[genesis_root] = genesis_block - block_cache = BlockCache() - - block = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=genesis_root, - state_root=Bytes32.zero(), - ) - block_root = hash_tree_root(block.block) - block_cache.add(block, peer_id) - - def fail_processing(s: Any, b: SignedBlock) -> Any: - raise Exception("Validation failed") - - head_sync = HeadSync( - block_cache=block_cache, - backfill=_null_backfill(), - process_block=fail_processing, - ) - - count, _ = await head_sync.process_all_processable(store) - - assert count == 0 - assert block_root not in block_cache # Removed despite failure - - class TestErrorHandling: """Tests for error handling during block processing.""" @@ -412,62 +323,6 @@ def fail_processing(s: Any, b: SignedBlock) -> Any: ) assert returned_store is store # Original store returned on error - async def test_sibling_error_does_not_block_other_siblings( - self, - genesis_block, - peer_id: PeerId, - ) -> None: - """Error processing one child doesn't prevent processing siblings.""" - genesis_root = hash_tree_root(genesis_block) - store = cast(Store, MockForkchoiceStore()) - store.blocks[genesis_root] = genesis_block - block_cache = BlockCache() - - # Two siblings - block1 = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=genesis_root, - state_root=Bytes32(b"\x01" * 32), - ) - - block2 = make_signed_block( - slot=Slot(2), - proposer_index=ValidatorIndex(1), - parent_root=genesis_root, - state_root=Bytes32(b"\x02" * 32), - ) - - block_cache.add(block1, peer_id) - block_cache.add(block2, peer_id) - - call_count = 0 - successful_roots: set[Bytes32] = set() - - def fail_first(s: Any, block: SignedBlock) -> Any: - nonlocal call_count - call_count += 1 - root = hash_tree_root(block.block) - if call_count == 1: - raise Exception("First fails") - successful_roots.add(root) - new_store = MockForkchoiceStore() - new_store.blocks = dict(s.blocks) - new_store.blocks[root] = object() - return new_store - - head_sync = HeadSync( - block_cache=block_cache, - backfill=_null_backfill(), - process_block=fail_first, - ) - - count, _ = await head_sync.process_all_processable(store) - - assert call_count == 2 # Both attempted - assert count == 1 # One succeeded - assert len(successful_roots) == 1 - class TestStorePropagation: """Tests for store propagation through descendant processing.""" @@ -583,68 +438,3 @@ def should_not_be_called(s: Any, b: SignedBlock) -> Any: ) assert returned_store is store assert call_count == 0 - - -class TestProcessAllProcessableConvergence: - """Tests for process_all_processable convergence.""" - - async def test_chain_processed_in_single_call( - self, - genesis_block, - peer_id: PeerId, - ) -> None: - """Chain A -> B -> C all processed in one process_all_processable call.""" - genesis_root = hash_tree_root(genesis_block) - store = cast(Store, MockForkchoiceStore()) - store.blocks[genesis_root] = genesis_block - block_cache = BlockCache() - - # Build chain: A -> B -> C, all with genesis as ultimate ancestor. - block_a = make_signed_block( - slot=Slot(1), - proposer_index=ValidatorIndex(0), - parent_root=genesis_root, - state_root=Bytes32(b"\x01" * 32), - ) - root_a = hash_tree_root(block_a.block) - - block_b = make_signed_block( - slot=Slot(2), - proposer_index=ValidatorIndex(0), - parent_root=root_a, - state_root=Bytes32(b"\x02" * 32), - ) - root_b = hash_tree_root(block_b.block) - - block_c = make_signed_block( - slot=Slot(3), - proposer_index=ValidatorIndex(0), - parent_root=root_b, - state_root=Bytes32(b"\x03" * 32), - ) - - block_cache.add(block_a, peer_id) - block_cache.add(block_b, peer_id) - block_cache.add(block_c, peer_id) - - processing_order: list[int] = [] - - def track_processing(s: Any, block: SignedBlock) -> Any: - processing_order.append(int(block.block.slot)) - root = hash_tree_root(block.block) - new_store = MockForkchoiceStore() - new_store.blocks = dict(s.blocks) - new_store.blocks[root] = object() - return new_store - - head_sync = HeadSync( - block_cache=block_cache, - backfill=_null_backfill(), - process_block=track_processing, - ) - - count, _ = await head_sync.process_all_processable(store) - - assert count == 3 - assert processing_order == [1, 2, 3] - assert len(block_cache) == 0 diff --git a/tests/lean_spec/subspecs/sync/test_peer_manager.py b/tests/lean_spec/subspecs/sync/test_peer_manager.py index 086cee4bd..32ea3dc42 100644 --- a/tests/lean_spec/subspecs/sync/test_peer_manager.py +++ b/tests/lean_spec/subspecs/sync/test_peer_manager.py @@ -158,13 +158,6 @@ def test_get_nonexistent_peer(self) -> None: manager = PeerManager() assert manager.get_peer(peer("16Uiu2HAmNonexistent")) is None - def test_clear(self, connected_peer_info: PeerInfo) -> None: - """Clear removes all peers.""" - manager = PeerManager() - manager.add_peer(connected_peer_info) - manager.clear() - assert len(manager) == 0 - class TestPeerManagerStatusTracking: """Tests for PeerManager status tracking.""" diff --git a/tests/lean_spec/subspecs/sync/test_service.py b/tests/lean_spec/subspecs/sync/test_service.py index 0df60e939..786978d4f 100644 --- a/tests/lean_spec/subspecs/sync/test_service.py +++ b/tests/lean_spec/subspecs/sync/test_service.py @@ -732,18 +732,6 @@ async def test_check_sync_trigger_noop_when_already_syncing( assert sync_service.state == SyncState.SYNCING - async def test_start_sync_noop_without_peer_finalized_slot( - self, - peer_id: PeerId, - ) -> None: - """Peer connected but no Status means no sync trigger.""" - service = create_mock_sync_service(peer_id) - assert service.state == SyncState.IDLE - - await service.start_sync() - - assert service.state == SyncState.IDLE - class TestSyncCompleteGuards: """Tests for _check_sync_complete early exits.""" diff --git a/tests/lean_spec/subspecs/validator/test_registry.py b/tests/lean_spec/subspecs/validator/test_registry.py index 7e3f3f029..bb996c3b7 100644 --- a/tests/lean_spec/subspecs/validator/test_registry.py +++ b/tests/lean_spec/subspecs/validator/test_registry.py @@ -331,23 +331,6 @@ def test_add_overwrites_existing_entry(self, km: XmssKeyManager) -> None: ValidatorIndex(5): (kp_att.attestation_secret, kp_prop.proposal_secret) } - def test_from_secret_keys(self, km: XmssKeyManager) -> None: - """Registry can be populated from a dictionary of key pairs.""" - kp_0 = km[ValidatorIndex(0)] - kp_2 = km[ValidatorIndex(2)] - - registry = ValidatorRegistry.from_secret_keys( - { - ValidatorIndex(0): (kp_0.attestation_secret, kp_0.proposal_secret), - ValidatorIndex(2): (kp_2.attestation_secret, kp_2.proposal_secret), - } - ) - - assert registry_state(registry) == { - ValidatorIndex(0): (kp_0.attestation_secret, kp_0.proposal_secret), - ValidatorIndex(2): (kp_2.attestation_secret, kp_2.proposal_secret), - } - def _write_manifest(path: Path, validators: list[dict[str, object]]) -> None: """Write a minimal manifest YAML file at path."""