diff --git a/tests/lean_spec/conftest.py b/tests/lean_spec/conftest.py new file mode 100644 index 00000000..e590bae8 --- /dev/null +++ b/tests/lean_spec/conftest.py @@ -0,0 +1,32 @@ +""" +Shared pytest fixtures for all lean_spec tests. + +Provides core fixtures used across multiple test modules. +Import these fixtures automatically via pytest discovery. +""" + +from __future__ import annotations + +import pytest + +from lean_spec.subspecs.containers import Block, State +from lean_spec.subspecs.forkchoice import Store +from tests.lean_spec.helpers import make_genesis_block, make_genesis_state + + +@pytest.fixture +def genesis_state() -> State: + """Genesis state with 3 validators at time 0.""" + return make_genesis_state(num_validators=3, genesis_time=0) + + +@pytest.fixture +def genesis_block(genesis_state: State) -> Block: + """Genesis block matching the genesis_state fixture.""" + return make_genesis_block(genesis_state) + + +@pytest.fixture +def base_store(genesis_state: State, genesis_block: Block) -> Store: + """Fork choice store initialized with genesis.""" + return Store.get_forkchoice_store(genesis_state, genesis_block) diff --git a/tests/lean_spec/helpers/__init__.py b/tests/lean_spec/helpers/__init__.py new file mode 100644 index 00000000..8a93bf8f --- /dev/null +++ b/tests/lean_spec/helpers/__init__.py @@ -0,0 +1,35 @@ +"""Test helpers for leanSpec unit tests.""" + +from .builders import ( + make_aggregated_attestation, + make_block, + make_bytes32, + make_genesis_block, + make_genesis_state, + make_mock_signature, + make_public_key_bytes, + make_signature, + make_signed_attestation, + make_signed_block, + make_validators, + make_validators_with_keys, +) +from .mocks import MockNoiseSession + +__all__ = [ + # Builders + "make_aggregated_attestation", + "make_block", + "make_bytes32", + "make_genesis_block", + "make_genesis_state", + "make_mock_signature", + "make_public_key_bytes", + "make_signature", + "make_signed_attestation", + "make_signed_block", + "make_validators", + "make_validators_with_keys", + # Mocks + "MockNoiseSession", +] diff --git a/tests/lean_spec/helpers/builders.py b/tests/lean_spec/helpers/builders.py new file mode 100644 index 00000000..09b79d7d --- /dev/null +++ b/tests/lean_spec/helpers/builders.py @@ -0,0 +1,275 @@ +""" +Factory functions for constructing test fixtures. + +Provides deterministic builders for all core container types. +Each function creates minimal valid instances suitable for unit tests. +""" + +from __future__ import annotations + +from lean_spec.subspecs.containers import ( + Attestation, + AttestationData, + Block, + BlockBody, + BlockWithAttestation, + Checkpoint, + SignedAttestation, + SignedBlockWithAttestation, + State, + Validator, +) +from lean_spec.subspecs.containers.attestation import AggregatedAttestation, AggregationBits +from lean_spec.subspecs.containers.block import BlockSignatures +from lean_spec.subspecs.containers.block.types import AggregatedAttestations, AttestationSignatures +from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.containers.state import Validators +from lean_spec.subspecs.koalabear import Fp +from lean_spec.subspecs.ssz.hash import hash_tree_root +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 ( + HashDigestList, + HashDigestVector, + HashTreeOpening, + Parameter, + Randomness, +) +from lean_spec.types import Bytes32, Bytes52, Uint64 + +# ----------------------------------------------------------------------------- +# Primitive Builders +# ----------------------------------------------------------------------------- + + +def make_bytes32(seed: int) -> Bytes32: + """Create a deterministic 32-byte value from a seed.""" + return Bytes32(bytes([seed % 256]) * 32) + + +def make_public_key_bytes(seed: int) -> bytes: + """ + Encode a deterministic XMSS public key. + + Constructs valid root and parameter vectors seeded by the input. + """ + root = HashDigestVector(data=[Fp(seed + i) for i in range(HashDigestVector.LENGTH)]) + parameter = Parameter(data=[Fp(seed + 100 + i) for i in range(Parameter.LENGTH)]) + public_key = PublicKey(root=root, parameter=parameter) + return public_key.encode_bytes() + + +# ----------------------------------------------------------------------------- +# Signature Builders +# ----------------------------------------------------------------------------- + + +def make_mock_signature() -> Signature: + """ + Create a minimal mock XMSS signature. + + Suitable for tests that require signature structure but skip verification. + """ + return Signature( + path=HashTreeOpening(siblings=HashDigestList(data=[])), + rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), + hashes=HashDigestList(data=[]), + ) + + +def make_signature(seed: int) -> Signature: + """ + Create a deterministic XMSS signature from a seed. + + Produces unique randomness values based on the seed. + """ + randomness = Randomness(data=[Fp(seed + 200 + i) for i in range(Randomness.LENGTH)]) + return Signature( + path=HashTreeOpening(siblings=HashDigestList(data=[])), + rho=randomness, + hashes=HashDigestList(data=[]), + ) + + +# ----------------------------------------------------------------------------- +# Validator Builders +# ----------------------------------------------------------------------------- + + +def make_validators(count: int) -> Validators: + """ + Build a validator registry with null public keys. + + Validators are indexed 0 through count-1. + """ + validators = [Validator(pubkey=Bytes52(b"\x00" * 52), index=Uint64(i)) for i in range(count)] + return Validators(data=validators) + + +def make_validators_with_keys(count: int) -> Validators: + """ + Build a validator registry with deterministic XMSS public keys. + + Each validator gets a unique key derived from their index. + """ + validators = [ + Validator(pubkey=Bytes52(make_public_key_bytes(i)), index=Uint64(i)) for i in range(count) + ] + return Validators(data=validators) + + +# ----------------------------------------------------------------------------- +# State Builders +# ----------------------------------------------------------------------------- + + +def make_genesis_state(num_validators: int = 3, genesis_time: int = 0) -> State: + """ + Create a genesis state with the specified validator count. + + Uses null public keys by default for simplicity. + """ + validators = make_validators(num_validators) + return State.generate_genesis(genesis_time=Uint64(genesis_time), validators=validators) + + +# ----------------------------------------------------------------------------- +# Block Builders +# ----------------------------------------------------------------------------- + + +def make_genesis_block(state: State) -> Block: + """ + Create a genesis block matching the given state. + + The state root is computed from the provided state. + """ + return Block( + slot=Slot(0), + proposer_index=Uint64(0), + parent_root=Bytes32.zero(), + state_root=hash_tree_root(state), + body=BlockBody(attestations=AggregatedAttestations(data=[])), + ) + + +def make_block( + state: State, + slot: Slot, + attestations: list[AggregatedAttestation], +) -> Block: + """ + Create a block at the given slot with attestations. + + Proposer index is derived from slot modulo validator count. + Parent root is computed from the state's latest block header. + """ + body = BlockBody(attestations=AggregatedAttestations(data=attestations)) + parent_root = hash_tree_root(state.latest_block_header) + proposer_index = Uint64(int(slot) % len(state.validators)) + + return Block( + slot=slot, + proposer_index=proposer_index, + parent_root=parent_root, + state_root=Bytes32.zero(), + body=body, + ) + + +def make_signed_block( + slot: Slot, + proposer_index: Uint64, + parent_root: Bytes32, + state_root: Bytes32, +) -> SignedBlockWithAttestation: + """ + Create a signed block with minimal valid structure. + + Includes a proposer attestation pointing to the new block. + """ + block = Block( + slot=slot, + proposer_index=proposer_index, + parent_root=parent_root, + state_root=state_root, + body=BlockBody(attestations=AggregatedAttestations(data=[])), + ) + + block_root = hash_tree_root(block) + + attestation = Attestation( + validator_id=proposer_index, + data=AttestationData( + slot=slot, + head=Checkpoint(root=block_root, slot=slot), + target=Checkpoint(root=block_root, slot=slot), + source=Checkpoint(root=parent_root, slot=Slot(0)), + ), + ) + + return SignedBlockWithAttestation( + message=BlockWithAttestation( + block=block, + proposer_attestation=attestation, + ), + signature=BlockSignatures( + attestation_signatures=AttestationSignatures(data=[]), + proposer_signature=make_mock_signature(), + ), + ) + + +# ----------------------------------------------------------------------------- +# Attestation Builders +# ----------------------------------------------------------------------------- + + +def make_aggregated_attestation( + participant_ids: list[int], + attestation_slot: Slot, + source: Checkpoint, + target: Checkpoint, +) -> AggregatedAttestation: + """ + Create an aggregated attestation from participating validators. + + Head checkpoint uses the target's root and slot. + """ + data = AttestationData( + slot=attestation_slot, + head=Checkpoint(root=target.root, slot=target.slot), + target=target, + source=source, + ) + + return AggregatedAttestation( + aggregation_bits=AggregationBits.from_validator_indices( + [Uint64(i) for i in participant_ids] + ), + data=data, + ) + + +def make_signed_attestation( + validator: Uint64, + target: Checkpoint, + source: Checkpoint | None = None, +) -> SignedAttestation: + """ + Construct a signed attestation for a single validator. + + Source defaults to a zero checkpoint if not provided. + """ + source_checkpoint = source or Checkpoint.default() + attestation_data = AttestationData( + slot=target.slot, + head=target, + target=target, + source=source_checkpoint, + ) + return SignedAttestation( + validator_id=validator, + message=attestation_data, + signature=make_mock_signature(), + ) diff --git a/tests/lean_spec/helpers/mocks.py b/tests/lean_spec/helpers/mocks.py new file mode 100644 index 00000000..05caf46b --- /dev/null +++ b/tests/lean_spec/helpers/mocks.py @@ -0,0 +1,58 @@ +""" +Mock classes for testing transport and networking layers. + +Each mock provides minimal implementations for isolated testing. +""" + +from __future__ import annotations + + +class MockNoiseSession: + """ + Mock NoiseSession for testing yamux multiplexing. + + Tracks written data and provides configurable read responses. + Does not perform actual encryption or handshake. + """ + + def __init__(self) -> None: + """Initialize with empty buffers.""" + self._written: list[bytes] = [] + self._to_read: list[bytes] = [] + self._closed = False + + @property + def written(self) -> list[bytes]: + """Data written through this session.""" + return self._written + + @property + def is_closed(self) -> bool: + """Whether the session has been closed.""" + return self._closed + + def queue_read(self, data: bytes) -> None: + """ + Queue data to be returned by the next read call. + + Multiple calls queue data in FIFO order. + """ + self._to_read.append(data) + + async def write(self, plaintext: bytes) -> None: + """Record written data for later inspection.""" + self._written.append(plaintext) + + async def read(self) -> bytes: + """ + Return queued data or empty bytes. + + Consumes queued data in FIFO order. + """ + if self._to_read: + return self._to_read.pop(0) + return b"" + + async def close(self) -> None: + """Mark the session as closed.""" + self._closed = True diff --git a/tests/lean_spec/subspecs/api/conftest.py b/tests/lean_spec/subspecs/api/conftest.py index 170119ff..581ebd30 100644 --- a/tests/lean_spec/subspecs/api/conftest.py +++ b/tests/lean_spec/subspecs/api/conftest.py @@ -1,45 +1,9 @@ -"""Shared test utilities and fixtures for API server tests.""" +""" +Shared pytest fixtures for API server tests. -from __future__ import annotations +Core fixtures inherited from parent conftest files. +""" -import pytest - -from lean_spec.subspecs.containers import Block, BlockBody, State, Validator -from lean_spec.subspecs.containers.block.types import AggregatedAttestations -from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.containers.state import Validators -from lean_spec.subspecs.forkchoice import Store -from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Bytes52, Uint64 - - -@pytest.fixture -def validators() -> Validators: - """Provide a minimal validator set for tests.""" - return Validators( - data=[Validator(pubkey=Bytes52(b"\x00" * 52), index=Uint64(i)) for i in range(3)] - ) - - -@pytest.fixture -def genesis_state(validators: Validators) -> State: - """Create a genesis state for testing.""" - return State.generate_genesis(Uint64(1704067200), validators) - - -@pytest.fixture -def genesis_block(genesis_state: State) -> Block: - """Create a genesis block for testing.""" - return Block( - slot=Slot(0), - proposer_index=Uint64(0), - parent_root=Bytes32.zero(), - state_root=hash_tree_root(genesis_state), - body=BlockBody(attestations=AggregatedAttestations(data=[])), - ) - - -@pytest.fixture -def store(genesis_state: State, genesis_block: Block) -> Store: - """Create a forkchoice store for testing.""" - return Store.get_forkchoice_store(genesis_state, genesis_block) +# All fixtures are inherited from tests/lean_spec/conftest.py via pytest discovery. +# This file exists to establish the conftest hierarchy and can be extended +# with API-specific fixtures as needed. diff --git a/tests/lean_spec/subspecs/api/test_server.py b/tests/lean_spec/subspecs/api/test_server.py index 41be7896..1964f4f7 100644 --- a/tests/lean_spec/subspecs/api/test_server.py +++ b/tests/lean_spec/subspecs/api/test_server.py @@ -53,12 +53,12 @@ def test_server_created_without_store(self) -> None: assert server.config == config assert server.store is None - def test_store_getter_provides_access_to_store(self, store: Store) -> None: + def test_store_getter_provides_access_to_store(self, base_store: Store) -> None: """Store getter callable provides access to the forkchoice store.""" config = ApiServerConfig() - server = ApiServer(config=config, store_getter=lambda: store) + server = ApiServer(config=config, store_getter=lambda: base_store) - assert server.store is store + assert server.store is base_store class TestHealthEndpoint: @@ -113,12 +113,12 @@ async def run_test() -> None: asyncio.run(run_test()) - def test_returns_ssz_state_when_store_available(self, store: Store) -> None: + def test_returns_ssz_state_when_store_available(self, base_store: Store) -> None: """Endpoint returns SSZ-encoded state as octet-stream.""" async def run_test() -> None: config = ApiServerConfig(port=15056) - server = ApiServer(config=config, store_getter=lambda: store) + server = ApiServer(config=config, store_getter=lambda: base_store) await server.start() @@ -205,12 +205,12 @@ async def run_test() -> None: class TestCheckpointSyncClientServerIntegration: """Integration tests for checkpoint sync client fetching from server.""" - def test_client_fetches_and_deserializes_state(self, store: Store) -> None: + def test_client_fetches_and_deserializes_state(self, base_store: Store) -> None: """Client successfully fetches and deserializes state from server.""" async def run_test() -> None: config = ApiServerConfig(port=15058) - server = ApiServer(config=config, store_getter=lambda: store) + server = ApiServer(config=config, store_getter=lambda: base_store) await server.start() diff --git a/tests/lean_spec/subspecs/containers/test_state_justified_slots.py b/tests/lean_spec/subspecs/containers/test_state_justified_slots.py index d58672a8..4e4d4cb9 100644 --- a/tests/lean_spec/subspecs/containers/test_state_justified_slots.py +++ b/tests/lean_spec/subspecs/containers/test_state_justified_slots.py @@ -9,68 +9,19 @@ import pytest -from lean_spec.subspecs.containers.attestation import ( - AggregatedAttestation, - AggregationBits, - AttestationData, -) -from lean_spec.subspecs.containers.block import Block, BlockBody -from lean_spec.subspecs.containers.block.types import AggregatedAttestations from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.containers.state import State, Validators -from lean_spec.subspecs.containers.validator import Validator -from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Bytes52, Uint64 - - -def _mk_validators(n: int) -> Validators: - return Validators(data=[Validator(pubkey=Bytes52.zero(), index=Uint64(i)) for i in range(n)]) - - -def _mk_block(state: State, slot: Slot, attestations: list[AggregatedAttestation]) -> Block: - body = BlockBody(attestations=AggregatedAttestations(data=attestations)) - - parent_root = hash_tree_root(state.latest_block_header) - proposer_index = Uint64(int(slot) % len(state.validators)) - - return Block( - slot=slot, - proposer_index=proposer_index, - parent_root=parent_root, - state_root=Bytes32.zero(), - body=body, - ) - - -def _mk_aggregated_attestation( - *, - participant_ids: list[int], - attestation_slot: Slot, - source: Checkpoint, - target: Checkpoint, -) -> AggregatedAttestation: - data = AttestationData( - slot=attestation_slot, - head=Checkpoint(root=target.root, slot=target.slot), - target=target, - source=source, - ) - - return AggregatedAttestation( - aggregation_bits=AggregationBits.from_validator_indices( - [Uint64(i) for i in participant_ids] - ), - data=data, - ) +from lean_spec.subspecs.containers.state import State +from lean_spec.types import Uint64 +from tests.lean_spec.helpers import make_aggregated_attestation, make_block, make_validators def test_justified_slots_do_not_include_finalized_boundary() -> None: - state = State.generate_genesis(genesis_time=Uint64(0), validators=_mk_validators(4)) + state = State.generate_genesis(genesis_time=Uint64(0), validators=make_validators(4)) # First post-genesis block at slot 1. state_slot_1 = state.process_slots(Slot(1)) - block_1 = _mk_block(state_slot_1, Slot(1), attestations=[]) + block_1 = make_block(state_slot_1, Slot(1), attestations=[]) post_1 = state_slot_1.process_block_header(block_1) # latest_finalized.slot is 0, so justified_slots starts at slot 1. @@ -79,7 +30,7 @@ def test_justified_slots_do_not_include_finalized_boundary() -> None: # Second block at slot 2 materializes parent slot 1, which is the first bit. post_1_slot_2 = post_1.process_slots(Slot(2)) - block_2 = _mk_block(post_1_slot_2, Slot(2), attestations=[]) + block_2 = make_block(post_1_slot_2, Slot(2), attestations=[]) post_2 = post_1_slot_2.process_block_header(block_2) assert len(post_2.justified_slots) == 1 @@ -88,43 +39,43 @@ def test_justified_slots_do_not_include_finalized_boundary() -> None: def test_justified_slots_rebases_when_finalization_advances() -> None: # Use 3 validators so a 2-of-3 aggregation is a supermajority. - state = State.generate_genesis(genesis_time=Uint64(0), validators=_mk_validators(3)) + state = State.generate_genesis(genesis_time=Uint64(0), validators=make_validators(3)) # Block 1 (slot 1): initializes history (stores slot 0 root), but no justified_slots bits yet. state = state.process_slots(Slot(1)) - block_1 = _mk_block(state, Slot(1), attestations=[]) + block_1 = make_block(state, Slot(1), attestations=[]) state = state.process_block(block_1) # Block 2 (slot 2): justify slot 1 with source=0 -> target=1. state = state.process_slots(Slot(2)) - block_2 = _mk_block(state, Slot(2), attestations=[]) + block_2 = make_block(state, Slot(2), attestations=[]) source_0 = Checkpoint(root=block_1.parent_root, slot=Slot(0)) target_1 = Checkpoint(root=block_2.parent_root, slot=Slot(1)) - att_0_to_1 = _mk_aggregated_attestation( + att_0_to_1 = make_aggregated_attestation( participant_ids=[0, 1], attestation_slot=Slot(2), source=source_0, target=target_1, ) - block_2 = _mk_block(state, Slot(2), attestations=[att_0_to_1]) + block_2 = make_block(state, Slot(2), attestations=[att_0_to_1]) state = state.process_block(block_2) # Block 3 (slot 3): justify slot 2 with source=1 -> target=2, which finalizes slot 1. state = state.process_slots(Slot(3)) - block_3 = _mk_block(state, Slot(3), attestations=[]) + block_3 = make_block(state, Slot(3), attestations=[]) source_1 = Checkpoint(root=block_2.parent_root, slot=Slot(1)) target_2 = Checkpoint(root=block_3.parent_root, slot=Slot(2)) - att_1_to_2 = _mk_aggregated_attestation( + att_1_to_2 = make_aggregated_attestation( participant_ids=[0, 1], attestation_slot=Slot(3), source=source_1, target=target_2, ) - block_3 = _mk_block(state, Slot(3), attestations=[att_1_to_2]) + block_3 = make_block(state, Slot(3), attestations=[att_1_to_2]) state = state.process_block(block_3) assert state.latest_finalized.slot == Slot(1) @@ -143,6 +94,6 @@ def test_is_slot_justified_raises_on_out_of_bounds() -> None: # For slots > finalized_slot, the bitfield must be long enough to cover the slot. # If it is not, this indicates an inconsistent state and should fail fast. with pytest.raises(IndexError): - State.generate_genesis(Uint64(0), _mk_validators(1)).justified_slots.is_slot_justified( + State.generate_genesis(Uint64(0), make_validators(1)).justified_slots.is_slot_justified( Slot(0), Slot(1) ) diff --git a/tests/lean_spec/subspecs/forkchoice/conftest.py b/tests/lean_spec/subspecs/forkchoice/conftest.py index 9750cf0a..f0a58a7c 100644 --- a/tests/lean_spec/subspecs/forkchoice/conftest.py +++ b/tests/lean_spec/subspecs/forkchoice/conftest.py @@ -1,16 +1,16 @@ -"""Shared test utilities for forkchoice tests.""" +""" +Shared pytest fixtures for forkchoice tests. + +Provides mock state for testing fork choice behavior. +""" + +from __future__ import annotations from typing import Type import pytest -from lean_spec.subspecs.containers import ( - AttestationData, - BlockBody, - Checkpoint, - SignedAttestation, - State, -) +from lean_spec.subspecs.containers import BlockBody, Checkpoint, State from lean_spec.subspecs.containers.block import AggregatedAttestations, BlockHeader from lean_spec.subspecs.containers.config import Config from lean_spec.subspecs.containers.slot import Slot @@ -21,20 +21,15 @@ JustificationValidators, JustifiedSlots, ) -from lean_spec.subspecs.koalabear import Fp from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.subspecs.xmss.constants import PROD_CONFIG -from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness from lean_spec.types import Bytes32, Uint64 class MockState(State): - """Mock state that exposes configurable ``latest_justified``.""" + """Mock state with configurable latest_justified checkpoint.""" def __init__(self, latest_justified: Checkpoint) -> None: - """Initialize a mock state with minimal defaults.""" - # Create minimal defaults for all required fields + """Initialize mock state with minimal defaults.""" genesis_config = Config( genesis_time=Uint64(0), ) @@ -61,31 +56,6 @@ def __init__(self, latest_justified: Checkpoint) -> None: ) -def build_signed_attestation( - validator: Uint64, - target: Checkpoint, - source: Checkpoint | None = None, -) -> SignedAttestation: - """Construct a SignedValidatorAttestation pointing to ``target``.""" - - source_checkpoint = source or Checkpoint.default() - attestation_data = AttestationData( - slot=target.slot, - head=target, - target=target, - source=source_checkpoint, - ) - return SignedAttestation( - validator_id=validator, - message=attestation_data, - signature=Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), - hashes=HashDigestList(data=[]), - ), - ) - - @pytest.fixture def mock_state_factory() -> Type[MockState]: """Factory fixture for creating MockState instances.""" diff --git a/tests/lean_spec/subspecs/forkchoice/test_time_management.py b/tests/lean_spec/subspecs/forkchoice/test_time_management.py index 8b1ce0a0..de86c9b4 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_time_management.py +++ b/tests/lean_spec/subspecs/forkchoice/test_time_management.py @@ -16,8 +16,7 @@ from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Bytes52, Uint64 - -from .conftest import build_signed_attestation +from tests.lean_spec.helpers import make_signed_attestation @pytest.fixture @@ -150,7 +149,7 @@ def test_tick_interval_actions_by_phase(self, sample_store: Store) -> None: # Add some test attestations for processing test_checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(1)) - sample_store.latest_new_attestations[Uint64(0)] = build_signed_attestation( + sample_store.latest_new_attestations[Uint64(0)] = make_signed_attestation( Uint64(0), test_checkpoint, ).message @@ -236,7 +235,7 @@ def test_accept_new_attestations_basic(self, sample_store: Store) -> None: """Test basic new attestation processing.""" # Add some new attestations checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(1)) - sample_store.latest_new_attestations[Uint64(0)] = build_signed_attestation( + sample_store.latest_new_attestations[Uint64(0)] = make_signed_attestation( Uint64(0), checkpoint, ).message @@ -266,7 +265,7 @@ def test_accept_new_attestations_multiple(self, sample_store: Store) -> None: ] for i, checkpoint in enumerate(checkpoints): - sample_store.latest_new_attestations[Uint64(i)] = build_signed_attestation( + sample_store.latest_new_attestations[Uint64(i)] = make_signed_attestation( Uint64(i), checkpoint, ).message @@ -338,7 +337,7 @@ def test_get_proposal_head_processes_attestations(self, sample_store: Store) -> # Add some new attestations (immutable update) checkpoint = Checkpoint(root=Bytes32(b"attestation" + b"\x00" * 21), slot=Slot(1)) new_new_attestations = dict(sample_store.latest_new_attestations) - new_new_attestations[Uint64(10)] = build_signed_attestation( + new_new_attestations[Uint64(10)] = make_signed_attestation( Uint64(10), checkpoint, ).message diff --git a/tests/lean_spec/subspecs/forkchoice/test_validator.py b/tests/lean_spec/subspecs/forkchoice/test_validator.py index 8826ec0b..6610fc27 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_validator.py +++ b/tests/lean_spec/subspecs/forkchoice/test_validator.py @@ -24,14 +24,11 @@ Validators, ) from lean_spec.subspecs.forkchoice import Store -from lean_spec.subspecs.koalabear import Fp from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.aggregation import SignatureKey -from lean_spec.subspecs.xmss.constants import PROD_CONFIG -from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness from lean_spec.types import Bytes32, Bytes52, Uint64 from lean_spec.types.validator import is_proposer +from tests.lean_spec.helpers import make_mock_signature @pytest.fixture @@ -120,32 +117,6 @@ def sample_store(config: Config, sample_state: State) -> Store: ) -def build_signed_attestation( - validator: Uint64, - slot: Slot, - head: Checkpoint, - source: Checkpoint, - target: Checkpoint, -) -> SignedAttestation: - """Create a signed attestation with a zeroed signature.""" - - data = AttestationData( - slot=slot, - head=head, - target=target, - source=source, - ) - return SignedAttestation( - validator_id=validator, - message=data, - signature=Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), - hashes=HashDigestList(data=[]), - ), - ) - - class TestBlockProduction: """Test validator block production functionality.""" @@ -180,19 +151,28 @@ def test_produce_block_with_attestations(self, sample_store: Store) -> None: head_block = sample_store.blocks[sample_store.head] # Add some attestations to the store - signed_5 = build_signed_attestation( - validator=Uint64(5), + head_checkpoint = Checkpoint(root=sample_store.head, slot=head_block.slot) + data_5 = AttestationData( slot=head_block.slot, - head=Checkpoint(root=sample_store.head, slot=head_block.slot), - source=sample_store.latest_justified, + head=head_checkpoint, target=sample_store.get_attestation_target(), + source=sample_store.latest_justified, + ) + signed_5 = SignedAttestation( + validator_id=Uint64(5), + message=data_5, + signature=make_mock_signature(), ) - signed_6 = build_signed_attestation( - validator=Uint64(6), + data_6 = AttestationData( slot=head_block.slot, - head=Checkpoint(root=sample_store.head, slot=head_block.slot), - source=sample_store.latest_justified, + head=head_checkpoint, target=sample_store.get_attestation_target(), + source=sample_store.latest_justified, + ) + signed_6 = SignedAttestation( + validator_id=Uint64(6), + message=data_6, + signature=make_mock_signature(), ) sample_store.latest_known_attestations[Uint64(5)] = signed_5.message sample_store.latest_known_attestations[Uint64(6)] = signed_6.message @@ -282,12 +262,17 @@ def test_produce_block_state_consistency(self, sample_store: Store) -> None: # Add some attestations to test state computation head_block = sample_store.blocks[sample_store.head] - signed_7 = build_signed_attestation( - validator=Uint64(7), + head_checkpoint = Checkpoint(root=sample_store.head, slot=head_block.slot) + data_7 = AttestationData( slot=head_block.slot, - head=Checkpoint(root=sample_store.head, slot=head_block.slot), - source=sample_store.latest_justified, + head=head_checkpoint, target=sample_store.get_attestation_target(), + source=sample_store.latest_justified, + ) + signed_7 = SignedAttestation( + validator_id=Uint64(7), + message=data_7, + signature=make_mock_signature(), ) sample_store.latest_known_attestations[Uint64(7)] = signed_7.message sig_key_7 = SignatureKey(Uint64(7), signed_7.message.data_root_bytes()) diff --git a/tests/lean_spec/subspecs/networking/conftest.py b/tests/lean_spec/subspecs/networking/conftest.py new file mode 100644 index 00000000..591f000f --- /dev/null +++ b/tests/lean_spec/subspecs/networking/conftest.py @@ -0,0 +1,60 @@ +""" +Shared pytest fixtures for networking tests. + +Provides peer ID and connection state fixtures. +""" + +from __future__ import annotations + +import pytest + +from lean_spec.subspecs.networking import PeerId +from lean_spec.subspecs.networking.peer.info import PeerInfo +from lean_spec.subspecs.networking.types import ConnectionState + +# ----------------------------------------------------------------------------- +# Peer ID Fixtures +# ----------------------------------------------------------------------------- + + +@pytest.fixture +def peer_id() -> PeerId: + """Primary test peer ID.""" + return PeerId.from_base58("16Uiu2HAmTestPeer123") + + +@pytest.fixture +def peer_id_2() -> PeerId: + """Secondary test peer ID.""" + return PeerId.from_base58("16Uiu2HAmTestPeer456") + + +@pytest.fixture +def peer_id_3() -> PeerId: + """Tertiary test peer ID.""" + return PeerId.from_base58("16Uiu2HAmTestPeer789") + + +# ----------------------------------------------------------------------------- +# Peer Info Fixtures +# ----------------------------------------------------------------------------- + + +@pytest.fixture +def connected_peer_info(peer_id: PeerId) -> PeerInfo: + """Peer info in connected state.""" + return PeerInfo( + peer_id=peer_id, + state=ConnectionState.CONNECTED, + address="/ip4/192.168.1.1/tcp/9000", + ) + + +@pytest.fixture +def disconnected_peer_info(peer_id: PeerId) -> PeerInfo: + """Peer info in disconnected state.""" + return PeerInfo( + peer_id=peer_id, + state=ConnectionState.DISCONNECTED, + address="/ip4/192.168.1.2/tcp/9000", + ) diff --git a/tests/lean_spec/subspecs/networking/test_network_service.py b/tests/lean_spec/subspecs/networking/test_network_service.py index a53ca5db..f662ae0e 100644 --- a/tests/lean_spec/subspecs/networking/test_network_service.py +++ b/tests/lean_spec/subspecs/networking/test_network_service.py @@ -10,23 +10,13 @@ from lean_spec.subspecs.chain.clock import SlotClock from lean_spec.subspecs.containers import ( - Attestation, AttestationData, - Block, - BlockBody, - BlockWithAttestation, Checkpoint, SignedBlockWithAttestation, ) from lean_spec.subspecs.containers.attestation import SignedAttestation -from lean_spec.subspecs.containers.block import BlockSignatures -from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestations, - AttestationSignatures, -) from lean_spec.subspecs.containers.slot import Slot 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.gossipsub.topic import GossipTopic, TopicKind from lean_spec.subspecs.networking.peer.info import PeerInfo @@ -44,10 +34,8 @@ from lean_spec.subspecs.sync.peer_manager import PeerManager from lean_spec.subspecs.sync.service import SyncService from lean_spec.subspecs.sync.states import SyncState -from lean_spec.subspecs.xmss.constants import PROD_CONFIG -from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness from lean_spec.types import Bytes32, Uint64 +from tests.lean_spec.helpers import make_mock_signature, make_signed_block @dataclass @@ -128,54 +116,6 @@ def on_gossip_attestation(self, attestation: SignedAttestation) -> "MockStore": return new_store -def create_mock_signature() -> Signature: - """Create a minimal mock signature for testing.""" - return Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), - hashes=HashDigestList(data=[]), - ) - - -def create_signed_block( - slot: Slot, - proposer_index: Uint64, - parent_root: Bytes32, - state_root: Bytes32, -) -> SignedBlockWithAttestation: - """Create a signed block with minimal valid structure for testing.""" - block = Block( - slot=slot, - proposer_index=proposer_index, - parent_root=parent_root, - state_root=state_root, - body=BlockBody(attestations=AggregatedAttestations(data=[])), - ) - - block_root = hash_tree_root(block) - - attestation = Attestation( - validator_id=proposer_index, - data=AttestationData( - slot=slot, - head=Checkpoint(root=block_root, slot=slot), - target=Checkpoint(root=block_root, slot=slot), - source=Checkpoint(root=parent_root, slot=Slot(0)), - ), - ) - - return SignedBlockWithAttestation( - message=BlockWithAttestation( - block=block, - proposer_attestation=attestation, - ), - signature=BlockSignatures( - attestation_signatures=AttestationSignatures(data=[]), - proposer_signature=create_mock_signature(), - ), - ) - - def create_sync_service(peer_id: PeerId) -> SyncService: """Create a SyncService with MockStore for testing.""" mock_store = MockStore(head_slot=0) @@ -224,7 +164,7 @@ def test_block_added_to_store_blocks_dict( genesis_root = sync_service.store.head - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -259,7 +199,7 @@ def test_store_head_updated_after_block( genesis_root = sync_service.store.head assert genesis_root == Bytes32.zero() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -291,7 +231,7 @@ def test_block_ignored_in_idle_state_store_unchanged( genesis_root = sync_service.store.head initial_blocks_count = len(sync_service.store.blocks) - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -331,7 +271,7 @@ def test_attestation_processed_by_store( target=Checkpoint(root=Bytes32.zero(), slot=Slot(1)), source=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), ), - signature=create_mock_signature(), + signature=make_mock_signature(), ) # Track initial attestations count @@ -375,7 +315,7 @@ def test_attestation_ignored_in_idle_state( target=Checkpoint(root=Bytes32.zero(), slot=Slot(1)), source=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), ), - signature=create_mock_signature(), + signature=make_mock_signature(), ) events: list[NetworkEvent] = [ @@ -478,7 +418,7 @@ def test_full_sync_flow_status_then_block( ) # Block to process (slot 1 - will exceed network finalized) - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -516,7 +456,7 @@ def test_block_before_status_is_ignored( genesis_root = sync_service.store.head # Block arrives BEFORE status - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -555,7 +495,7 @@ def test_multiple_blocks_chain_extension( genesis_root = sync_service.store.head # Create chain: genesis -> block1 -> block2 - block1 = create_signed_block( + block1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -563,7 +503,7 @@ def test_multiple_blocks_chain_extension( ) block1_root = hash_tree_root(block1.message.block) - block2 = create_signed_block( + block2 = make_signed_block( slot=Slot(2), proposer_index=Uint64(1), parent_root=block1_root, diff --git a/tests/lean_spec/subspecs/networking/transport/yamux/conftest.py b/tests/lean_spec/subspecs/networking/transport/yamux/conftest.py new file mode 100644 index 00000000..e02f2f7b --- /dev/null +++ b/tests/lean_spec/subspecs/networking/transport/yamux/conftest.py @@ -0,0 +1,59 @@ +""" +Shared pytest fixtures for yamux multiplexing tests. + +Provides mock session and stream factories. +""" + +from __future__ import annotations + +import pytest + +from lean_spec.subspecs.networking.transport.yamux.session import YamuxSession, YamuxStream +from tests.lean_spec.helpers import MockNoiseSession + +# ----------------------------------------------------------------------------- +# Session Fixtures +# ----------------------------------------------------------------------------- + + +@pytest.fixture +def mock_noise_session() -> MockNoiseSession: + """Mock NoiseSession for yamux testing.""" + return MockNoiseSession() + + +@pytest.fixture +def make_yamux_session(mock_noise_session: MockNoiseSession): + """ + Factory fixture for YamuxSession instances. + + Returns a callable that creates sessions with configurable initiator status. + """ + + def _make(is_initiator: bool = True) -> YamuxSession: + return YamuxSession(noise=mock_noise_session, is_initiator=is_initiator) + + return _make + + +@pytest.fixture +def make_yamux_stream(): + """ + Factory fixture for YamuxStream instances. + + Returns a callable that creates streams with configurable parameters. + """ + + def _make( + stream_id: int = 1, + is_initiator: bool = True, + ) -> YamuxStream: + noise = MockNoiseSession() + session = YamuxSession(noise=noise, is_initiator=is_initiator) + return YamuxStream( + stream_id=stream_id, + session=session, + is_initiator=is_initiator, + ) + + return _make diff --git a/tests/lean_spec/subspecs/networking/transport/yamux/test_security.py b/tests/lean_spec/subspecs/networking/transport/yamux/test_security.py index 16d36328..31195abe 100644 --- a/tests/lean_spec/subspecs/networking/transport/yamux/test_security.py +++ b/tests/lean_spec/subspecs/networking/transport/yamux/test_security.py @@ -31,6 +31,7 @@ YamuxSession, YamuxStream, ) +from tests.lean_spec.helpers import MockNoiseSession class TestMaxFrameSizeEnforcement: @@ -472,23 +473,3 @@ def _create_mock_session(is_initiator: bool) -> YamuxSession: """Create a mock YamuxSession for testing.""" noise = MockNoiseSession() return YamuxSession(noise=noise, is_initiator=is_initiator) - - -class MockNoiseSession: - """Mock NoiseSession for testing yamux.""" - - def __init__(self) -> None: - self._written: list[bytes] = [] - self._to_read: list[bytes] = [] - self._closed = False - - async def write(self, plaintext: bytes) -> None: - self._written.append(plaintext) - - async def read(self) -> bytes: - if self._to_read: - return self._to_read.pop(0) - return b"" - - async def close(self) -> None: - self._closed = True diff --git a/tests/lean_spec/subspecs/networking/transport/yamux/test_session.py b/tests/lean_spec/subspecs/networking/transport/yamux/test_session.py index 99d2bbdd..150d0755 100644 --- a/tests/lean_spec/subspecs/networking/transport/yamux/test_session.py +++ b/tests/lean_spec/subspecs/networking/transport/yamux/test_session.py @@ -16,6 +16,7 @@ YamuxSession, YamuxStream, ) +from tests.lean_spec.helpers import MockNoiseSession class TestSessionConstants: @@ -591,23 +592,3 @@ def _create_mock_session(is_initiator: bool) -> YamuxSession: """Create a mock YamuxSession for testing.""" noise = MockNoiseSession() return YamuxSession(noise=noise, is_initiator=is_initiator) - - -class MockNoiseSession: - """Mock NoiseSession for testing yamux.""" - - def __init__(self) -> None: - self._written: list[bytes] = [] - self._to_read: list[bytes] = [] - self._closed = False - - async def write(self, plaintext: bytes) -> None: - self._written.append(plaintext) - - async def read(self) -> bytes: - if self._to_read: - return self._to_read.pop(0) - return b"" - - async def close(self) -> None: - self._closed = True diff --git a/tests/lean_spec/subspecs/sync/conftest.py b/tests/lean_spec/subspecs/sync/conftest.py index 4d8e36bd..8d0d3366 100644 --- a/tests/lean_spec/subspecs/sync/conftest.py +++ b/tests/lean_spec/subspecs/sync/conftest.py @@ -1,61 +1,48 @@ -"""Shared test utilities and fixtures for sync service tests.""" +""" +Shared pytest fixtures for sync service tests. + +Provides sync-specific fixtures. +Core fixtures inherited from parent conftest files. +""" from __future__ import annotations import pytest -from lean_spec.subspecs.containers import ( - Attestation, - AttestationData, - Block, - BlockBody, - BlockWithAttestation, - Checkpoint, - SignedBlockWithAttestation, - State, - Validator, -) -from lean_spec.subspecs.containers.block import BlockSignatures -from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestations, - AttestationSignatures, -) +from lean_spec.subspecs.containers import Checkpoint from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.containers.state import Validators -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.xmss.constants import PROD_CONFIG -from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness -from lean_spec.types import Bytes32, Bytes52, Uint64 +from lean_spec.types import Bytes32 + +# ----------------------------------------------------------------------------- +# Peer Fixtures +# ----------------------------------------------------------------------------- @pytest.fixture def peer_id() -> PeerId: - """Provide a sample peer ID for tests.""" + """Primary test peer ID.""" return PeerId.from_base58("16Uiu2HAmTestPeer123") @pytest.fixture def peer_id_2() -> PeerId: - """Provide a second sample peer ID for tests.""" + """Secondary test peer ID.""" return PeerId.from_base58("16Uiu2HAmTestPeer456") @pytest.fixture def peer_id_3() -> PeerId: - """Provide a third sample peer ID for tests.""" + """Tertiary test peer ID.""" return PeerId.from_base58("16Uiu2HAmTestPeer789") @pytest.fixture def connected_peer_info(peer_id: PeerId) -> PeerInfo: - """Provide a connected peer info for tests.""" + """Peer info in connected state.""" return PeerInfo( peer_id=peer_id, state=ConnectionState.CONNECTED, @@ -65,7 +52,7 @@ def connected_peer_info(peer_id: PeerId) -> PeerInfo: @pytest.fixture def disconnected_peer_info(peer_id: PeerId) -> PeerInfo: - """Provide a disconnected peer info for tests.""" + """Peer info in disconnected state.""" return PeerInfo( peer_id=peer_id, state=ConnectionState.DISCONNECTED, @@ -73,116 +60,21 @@ def disconnected_peer_info(peer_id: PeerId) -> PeerInfo: ) +# ----------------------------------------------------------------------------- +# Sync-Specific Fixtures +# ----------------------------------------------------------------------------- + + @pytest.fixture def sample_checkpoint() -> Checkpoint: - """Provide a sample checkpoint for tests.""" + """Sample checkpoint for sync tests.""" return Checkpoint(root=Bytes32.zero(), slot=Slot(100)) @pytest.fixture def sample_status(sample_checkpoint: Checkpoint) -> Status: - """Provide a sample Status message for tests.""" + """Sample Status message for sync tests.""" return Status( finalized=sample_checkpoint, head=Checkpoint(root=Bytes32.zero(), slot=Slot(150)), ) - - -@pytest.fixture -def genesis_state() -> State: - """Provide a genesis state for tests.""" - validators = Validators( - data=[Validator(pubkey=Bytes52(b"\x00" * 52), index=Uint64(i)) for i in range(3)] - ) - return State.generate_genesis(genesis_time=Uint64(0), validators=validators) - - -@pytest.fixture -def genesis_block(genesis_state: State) -> Block: - """Provide a genesis block for tests.""" - return Block( - slot=Slot(0), - proposer_index=Uint64(0), - parent_root=Bytes32.zero(), - state_root=hash_tree_root(genesis_state), - body=BlockBody(attestations=AggregatedAttestations(data=[])), - ) - - -@pytest.fixture -def base_store(genesis_state: State, genesis_block: Block) -> Store: - """Provide a base Store initialized with genesis for tests.""" - return Store.get_forkchoice_store(genesis_state, genesis_block) - - -def create_mock_signature() -> Signature: - """Create a minimal mock signature for testing.""" - return Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), - hashes=HashDigestList(data=[]), - ) - - -def create_signed_block( - slot: Slot, - proposer_index: Uint64, - parent_root: Bytes32, - state_root: Bytes32, -) -> SignedBlockWithAttestation: - """Create a signed block with minimal valid structure for testing.""" - block = Block( - slot=slot, - proposer_index=proposer_index, - parent_root=parent_root, - state_root=state_root, - body=BlockBody(attestations=AggregatedAttestations(data=[])), - ) - - block_root = hash_tree_root(block) - - attestation = Attestation( - validator_id=proposer_index, - data=AttestationData( - slot=slot, - head=Checkpoint(root=block_root, slot=slot), - target=Checkpoint(root=block_root, slot=slot), - source=Checkpoint(root=parent_root, slot=Slot(0)), - ), - ) - - return SignedBlockWithAttestation( - message=BlockWithAttestation( - block=block, - proposer_attestation=attestation, - ), - signature=BlockSignatures( - attestation_signatures=AttestationSignatures(data=[]), - proposer_signature=create_mock_signature(), - ), - ) - - -@pytest.fixture -def signed_block_factory() -> type: - """Factory fixture for creating signed blocks.""" - - class SignedBlockFactory: - """Factory for creating signed blocks with different parameters.""" - - @staticmethod - def create( - slot: int = 1, - proposer_index: int = 0, - parent_root: Bytes32 | None = None, - state_root: Bytes32 | None = None, - ) -> SignedBlockWithAttestation: - """Create a signed block with the given parameters.""" - return create_signed_block( - slot=Slot(slot), - proposer_index=Uint64(proposer_index), - parent_root=parent_root or Bytes32.zero(), - state_root=state_root or Bytes32.zero(), - ) - - return SignedBlockFactory diff --git a/tests/lean_spec/subspecs/sync/test_backfill_sync.py b/tests/lean_spec/subspecs/sync/test_backfill_sync.py index d057c7da..ac402f8e 100644 --- a/tests/lean_spec/subspecs/sync/test_backfill_sync.py +++ b/tests/lean_spec/subspecs/sync/test_backfill_sync.py @@ -17,8 +17,7 @@ from lean_spec.subspecs.sync.config import MAX_BACKFILL_DEPTH, MAX_BLOCKS_PER_REQUEST from lean_spec.subspecs.sync.peer_manager import PeerManager from lean_spec.types import Bytes32, Uint64 - -from .conftest import create_signed_block +from tests.lean_spec.helpers import make_signed_block class MockNetworkRequester: @@ -77,7 +76,7 @@ def test_fetch_single_missing_block( ) -> None: """Fetching a single missing block adds it to cache.""" # Create a block that will be "on the network" - block = create_signed_block( + block = make_signed_block( slot=Slot(10), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -103,7 +102,7 @@ def test_recursive_parent_chain_resolution( """Backfill recursively fetches missing parents up the chain.""" # Create a chain: grandparent -> parent -> child # Only child's root is initially requested, but we should fetch all - grandparent = create_signed_block( + grandparent = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), # Genesis as parent (known) @@ -111,7 +110,7 @@ def test_recursive_parent_chain_resolution( ) grandparent_root = network.add_block(grandparent) - parent = create_signed_block( + parent = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=grandparent_root, @@ -119,7 +118,7 @@ def test_recursive_parent_chain_resolution( ) parent_root = network.add_block(parent) - child = create_signed_block( + child = make_signed_block( slot=Slot(3), proposer_index=Uint64(0), parent_root=parent_root, @@ -168,7 +167,7 @@ def test_skips_already_cached_blocks( peer_id: PeerId, ) -> None: """Blocks already in cache are not re-requested.""" - block = create_signed_block( + block = make_signed_block( slot=Slot(5), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -250,7 +249,7 @@ def test_fill_all_orphans_fetches_missing_parents( ) -> None: """fill_all_orphans fetches parents for all orphan blocks in cache.""" # Create a parent block on the network - parent = create_signed_block( + parent = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -259,7 +258,7 @@ def test_fill_all_orphans_fetches_missing_parents( parent_root = network.add_block(parent) # Add child as orphan (parent not in cache) - child = create_signed_block( + child = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=parent_root, @@ -284,7 +283,7 @@ def test_shared_parent_deduplicated( ) -> None: """Multiple orphans with same parent only trigger one request for that parent.""" # Shared parent - parent = create_signed_block( + parent = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -293,13 +292,13 @@ def test_shared_parent_deduplicated( parent_root = network.add_block(parent) # Two children with same parent - child1 = create_signed_block( + child1 = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=parent_root, state_root=Bytes32(b"\x01" * 32), ) - child2 = create_signed_block( + child2 = make_signed_block( slot=Slot(2), proposer_index=Uint64(1), parent_root=parent_root, diff --git a/tests/lean_spec/subspecs/sync/test_block_cache.py b/tests/lean_spec/subspecs/sync/test_block_cache.py index 173a998b..37acf887 100644 --- a/tests/lean_spec/subspecs/sync/test_block_cache.py +++ b/tests/lean_spec/subspecs/sync/test_block_cache.py @@ -11,8 +11,7 @@ from lean_spec.subspecs.sync.block_cache import BlockCache, PendingBlock from lean_spec.subspecs.sync.config import MAX_CACHED_BLOCKS from lean_spec.types import Bytes32, Uint64 - -from .conftest import create_signed_block +from tests.lean_spec.helpers import make_signed_block class TestPendingBlock: @@ -20,7 +19,7 @@ class TestPendingBlock: def test_create_pending_block(self, peer_id: PeerId) -> None: """PendingBlock can be created with required fields.""" - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -46,7 +45,7 @@ def test_create_pending_block(self, peer_id: PeerId) -> None: def test_pending_block_default_received_at(self, peer_id: PeerId) -> None: """PendingBlock sets received_at to current time by default.""" before = time() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -67,7 +66,7 @@ def test_pending_block_default_received_at(self, peer_id: PeerId) -> None: def test_pending_block_custom_backfill_depth(self, peer_id: PeerId) -> None: """PendingBlock can be created with custom backfill depth.""" - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -101,7 +100,7 @@ def test_empty_cache(self) -> None: def test_add_block(self, peer_id: PeerId) -> None: """Adding a block stores it in the cache.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -118,7 +117,7 @@ def test_add_block(self, peer_id: PeerId) -> None: def test_contains_block(self, peer_id: PeerId) -> None: """Contains check works for cached blocks.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -133,7 +132,7 @@ def test_contains_block(self, peer_id: PeerId) -> None: def test_get_block(self, peer_id: PeerId) -> None: """Getting a block by root returns the PendingBlock.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -157,7 +156,7 @@ def test_get_nonexistent_block(self) -> None: def test_remove_block(self, peer_id: PeerId) -> None: """Removing a block returns it and removes from cache.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -183,7 +182,7 @@ def test_clear_cache(self, peer_id: PeerId) -> None: """Clear removes all blocks from the cache.""" cache = BlockCache() for i in range(5): - block = create_signed_block( + block = make_signed_block( slot=Slot(i + 1), proposer_index=Uint64(0), parent_root=Bytes32(i.to_bytes(32, "big")), @@ -206,7 +205,7 @@ class TestBlockCacheDeduplication: def test_adding_same_block_twice_returns_existing(self, peer_id: PeerId) -> None: """Adding the same block twice returns the existing PendingBlock.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -224,7 +223,7 @@ def test_deduplication_preserves_original_peer( ) -> None: """Deduplication keeps the original peer, not the second sender.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -244,7 +243,7 @@ class TestBlockCacheOrphanTracking: def test_mark_orphan(self, peer_id: PeerId) -> None: """Marking a block as orphan adds it to the orphan set.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -259,7 +258,7 @@ def test_mark_orphan(self, peer_id: PeerId) -> None: def test_mark_orphan_idempotent(self, peer_id: PeerId) -> None: """Marking the same block as orphan multiple times does not duplicate.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -283,7 +282,7 @@ def test_mark_orphan_nonexistent_block_does_nothing(self) -> None: def test_unmark_orphan(self, peer_id: PeerId) -> None: """Unmarking an orphan removes it from the orphan set.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -308,7 +307,7 @@ def test_unmark_orphan_nonexistent_does_nothing(self) -> None: def test_remove_clears_orphan_status(self, peer_id: PeerId) -> None: """Removing a block also removes its orphan status.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -328,7 +327,7 @@ 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 = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=parent_root, @@ -349,13 +348,13 @@ def test_get_orphan_parents_deduplicates(self, peer_id: PeerId) -> None: common_parent = Bytes32(b"\x01" * 32) # Two orphan blocks with the same missing parent - block1 = create_signed_block( + block1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=common_parent, state_root=Bytes32.zero(), ) - block2 = create_signed_block( + block2 = make_signed_block( slot=Slot(1), proposer_index=Uint64(1), parent_root=common_parent, @@ -378,7 +377,7 @@ def test_get_orphan_parents_excludes_cached_parents(self, peer_id: PeerId) -> No cache = BlockCache() # Add a parent block - parent_block = create_signed_block( + parent_block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -387,7 +386,7 @@ def test_get_orphan_parents_excludes_cached_parents(self, peer_id: PeerId) -> No parent_pending = cache.add(parent_block, peer_id) # Add child block that references the cached parent - child_block = create_signed_block( + child_block = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=parent_pending.root, @@ -417,7 +416,7 @@ def test_get_children_single_child(self, peer_id: PeerId) -> None: """get_children returns the single child of a parent.""" cache = BlockCache() parent_root = Bytes32(b"\x01" * 32) - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=parent_root, @@ -437,13 +436,13 @@ def test_get_children_multiple_children(self, peer_id: PeerId) -> None: parent_root = Bytes32(b"\x01" * 32) # Two blocks with the same parent (competing blocks) - block1 = create_signed_block( + block1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=parent_root, state_root=Bytes32.zero(), ) - block2 = create_signed_block( + block2 = make_signed_block( slot=Slot(1), proposer_index=Uint64(1), parent_root=parent_root, @@ -466,19 +465,19 @@ def test_get_children_sorted_by_slot(self, peer_id: PeerId) -> None: parent_root = Bytes32(b"\x01" * 32) # Add blocks out of order - block_slot3 = create_signed_block( + block_slot3 = make_signed_block( slot=Slot(3), proposer_index=Uint64(0), parent_root=parent_root, state_root=Bytes32(b"\x03" * 32), ) - block_slot1 = create_signed_block( + block_slot1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=parent_root, state_root=Bytes32(b"\x01" * 32), ) - block_slot2 = create_signed_block( + block_slot2 = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=parent_root, @@ -500,7 +499,7 @@ def test_remove_clears_parent_index(self, peer_id: PeerId) -> None: """Removing a block clears it from the parent-to-children index.""" cache = BlockCache() parent_root = Bytes32(b"\x01" * 32) - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=parent_root, @@ -524,7 +523,7 @@ def test_cache_respects_max_capacity(self, peer_id: PeerId) -> None: # Add more blocks than the limit for i in range(MAX_CACHED_BLOCKS + 10): - block = create_signed_block( + block = make_signed_block( slot=Slot(i + 1), proposer_index=Uint64(0), parent_root=Bytes32(i.to_bytes(32, "big")), @@ -541,7 +540,7 @@ def test_fifo_eviction_oldest_first(self, peer_id: PeerId) -> None: # Track the first blocks added first_roots = [] for i in range(MAX_CACHED_BLOCKS): - block = create_signed_block( + block = make_signed_block( slot=Slot(i + 1), proposer_index=Uint64(0), parent_root=Bytes32(i.to_bytes(32, "big")), @@ -557,7 +556,7 @@ def test_fifo_eviction_oldest_first(self, peer_id: PeerId) -> None: # Add 10 more blocks to trigger eviction for i in range(10): - block = create_signed_block( + block = make_signed_block( slot=Slot(MAX_CACHED_BLOCKS + i + 1), proposer_index=Uint64(0), parent_root=Bytes32((MAX_CACHED_BLOCKS + i).to_bytes(32, "big")), @@ -576,7 +575,7 @@ def test_eviction_clears_orphan_status(self, peer_id: PeerId) -> None: cache = BlockCache() # Add a block and mark it as orphan - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32(b"\x01" * 32), @@ -589,7 +588,7 @@ def test_eviction_clears_orphan_status(self, peer_id: PeerId) -> None: # Fill cache to trigger eviction of the first block for i in range(MAX_CACHED_BLOCKS): - new_block = create_signed_block( + new_block = make_signed_block( slot=Slot(i + 2), proposer_index=Uint64(0), parent_root=Bytes32((i + 1).to_bytes(32, "big")), @@ -620,7 +619,7 @@ def test_get_processable_no_parents_in_store(self, peer_id: PeerId) -> None: mock_store = MagicMock() mock_store.blocks = {} - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32(b"\x01" * 32), @@ -640,7 +639,7 @@ def test_get_processable_finds_block_with_parent_in_store(self, peer_id: PeerId) mock_store = MagicMock() mock_store.blocks = {parent_root: MagicMock()} - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=parent_root, @@ -662,19 +661,19 @@ def test_get_processable_sorted_by_slot(self, peer_id: PeerId) -> None: mock_store.blocks = {parent_root: MagicMock()} # Add blocks out of order - block3 = create_signed_block( + block3 = make_signed_block( slot=Slot(3), proposer_index=Uint64(0), parent_root=parent_root, state_root=Bytes32(b"\x03" * 32), ) - block1 = create_signed_block( + block1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=parent_root, state_root=Bytes32(b"\x01" * 32), ) - block2 = create_signed_block( + block2 = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=parent_root, @@ -699,7 +698,7 @@ class TestBlockCacheBackfillDepth: def test_add_with_backfill_depth(self, peer_id: PeerId) -> None: """Adding a block with backfill depth tracks the depth.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -713,7 +712,7 @@ def test_add_with_backfill_depth(self, peer_id: PeerId) -> None: def test_add_default_backfill_depth_is_zero(self, peer_id: PeerId) -> None: """Adding a block without backfill depth defaults to 0.""" cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), diff --git a/tests/lean_spec/subspecs/sync/test_head_sync.py b/tests/lean_spec/subspecs/sync/test_head_sync.py index 6e511cff..b2257374 100644 --- a/tests/lean_spec/subspecs/sync/test_head_sync.py +++ b/tests/lean_spec/subspecs/sync/test_head_sync.py @@ -17,8 +17,7 @@ from lean_spec.subspecs.sync.block_cache import BlockCache from lean_spec.subspecs.sync.head_sync import HeadSync from lean_spec.types import Bytes32, Uint64 - -from .conftest import create_signed_block +from tests.lean_spec.helpers import make_signed_block class MockStore: @@ -64,7 +63,7 @@ def track_processing(s: Any, block: SignedBlockWithAttestation) -> Any: process_block=track_processing, ) - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -95,7 +94,7 @@ def test_block_with_unknown_parent_cached_and_triggers_backfill( ) unknown_parent = Bytes32(b"\x01" * 32) - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=unknown_parent, @@ -121,7 +120,7 @@ def test_duplicate_block_skipped( genesis_root, store = genesis_setup # Add a block that's already in the store - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -159,7 +158,7 @@ def test_cached_children_processed_when_parent_arrives( block_cache = BlockCache() # Create parent and child - parent = create_signed_block( + parent = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -167,7 +166,7 @@ def test_cached_children_processed_when_parent_arrives( ) parent_root = hash_tree_root(parent.message.block) - child = create_signed_block( + child = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=parent_root, @@ -215,7 +214,7 @@ def test_chain_of_descendants_processed_in_slot_order( blocks = [] parent_root = genesis_root for i in range(1, 5): - block = create_signed_block( + block = make_signed_block( slot=Slot(i), proposer_index=Uint64(0), parent_root=parent_root, @@ -265,13 +264,13 @@ def test_processes_all_blocks_with_known_parents( block_cache = BlockCache() # Create two independent blocks with genesis as parent - block1 = create_signed_block( + block1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, state_root=Bytes32(b"\x01" * 32), ) - block2 = create_signed_block( + block2 = make_signed_block( slot=Slot(1), proposer_index=Uint64(1), parent_root=genesis_root, @@ -313,7 +312,7 @@ def test_processing_failure_removes_block_from_cache( store.blocks[genesis_root] = genesis_block block_cache = BlockCache() - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -359,7 +358,7 @@ def fail_processing(s: Any, b: SignedBlockWithAttestation) -> Any: process_block=fail_processing, ) - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -384,14 +383,14 @@ def test_sibling_error_does_not_block_other_siblings( block_cache = BlockCache() # Two siblings - block1 = create_signed_block( + block1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, state_root=Bytes32(b"\x01" * 32), ) - block2 = create_signed_block( + block2 = make_signed_block( slot=Slot(2), proposer_index=Uint64(1), parent_root=genesis_root, diff --git a/tests/lean_spec/subspecs/sync/test_service.py b/tests/lean_spec/subspecs/sync/test_service.py index 1326343a..592812a6 100644 --- a/tests/lean_spec/subspecs/sync/test_service.py +++ b/tests/lean_spec/subspecs/sync/test_service.py @@ -23,8 +23,7 @@ from lean_spec.subspecs.sync.service import SyncService from lean_spec.subspecs.sync.states import SyncState from lean_spec.types import Bytes32, Uint64 - -from .conftest import create_signed_block +from tests.lean_spec.helpers import make_signed_block class MockNetworkRequester: @@ -142,7 +141,7 @@ def test_stays_syncing_with_orphans( sync_service._state = SyncState.SYNCING # Add an orphan to the cache - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32(b"\x01" * 32), @@ -227,7 +226,7 @@ def test_ignores_gossip_in_idle_state( """Gossip blocks are ignored when in IDLE state.""" assert sync_service.state == SyncState.IDLE - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32.zero(), @@ -251,7 +250,7 @@ def test_processes_gossip_in_syncing_state( # Get genesis root from store genesis_root = sync_service.store.head - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=genesis_root, @@ -272,7 +271,7 @@ def test_caches_orphan_in_syncing_state( sync_service._state = SyncState.SYNCING # Block with unknown parent - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32(b"\x01" * 32), @@ -334,13 +333,13 @@ def test_progress_tracks_cache_state( ) -> None: """Progress includes cache size and orphan count.""" # Add blocks to cache - block1 = create_signed_block( + block1 = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32(b"\x01" * 32), state_root=Bytes32(b"\x01" * 32), ) - block2 = create_signed_block( + block2 = make_signed_block( slot=Slot(2), proposer_index=Uint64(0), parent_root=Bytes32(b"\x02" * 32), @@ -369,7 +368,7 @@ def test_reset_clears_all_state( sync_service._state = SyncState.SYNCED sync_service._blocks_processed = 100 - block = create_signed_block( + block = make_signed_block( slot=Slot(1), proposer_index=Uint64(0), parent_root=Bytes32(b"\x01" * 32),