diff --git a/src/lean_spec/subspecs/sync/service.py b/src/lean_spec/subspecs/sync/service.py index 66feed21b..24dfdc736 100644 --- a/src/lean_spec/subspecs/sync/service.py +++ b/src/lean_spec/subspecs/sync/service.py @@ -7,6 +7,7 @@ from __future__ import annotations import logging +from collections import deque from collections.abc import Callable, Coroutine from dataclasses import dataclass, field @@ -137,17 +138,21 @@ class SyncService: _blocks_processed: int = field(default=0) """Counter for processed blocks.""" - _pending_attestations: list[SignedAttestation] = field(default_factory=list) + _pending_attestations: deque[SignedAttestation] = field( + default_factory=lambda: deque(maxlen=MAX_PENDING_ATTESTATIONS) + ) """ Attestations queued for replay after the next block lands. An attestation referencing a not-yet-received block fails validation. Buffering avoids dropping votes that arrived slightly out of order. + + Bounded so overflow drops the oldest entry first. """ - _pending_aggregated_attestations: list[SignedAggregatedAttestation] = field( - default_factory=list + _pending_aggregated_attestations: deque[SignedAggregatedAttestation] = field( + default_factory=lambda: deque(maxlen=MAX_PENDING_ATTESTATIONS) ) """ Aggregated attestations awaiting block processing. @@ -478,8 +483,6 @@ async def on_gossip_attestation( # # Cap drops oldest on overflow: newer attestations are likelier to land soon. self._pending_attestations.append(attestation) - if len(self._pending_attestations) > MAX_PENDING_ATTESTATIONS: - self._pending_attestations = self._pending_attestations[-MAX_PENDING_ATTESTATIONS:] async def on_gossip_aggregated_attestation( self, @@ -522,10 +525,6 @@ async def on_gossip_aggregated_attestation( # # Cap drops oldest on overflow: newer aggregates are likelier to land soon. self._pending_aggregated_attestations.append(signed_attestation) - if len(self._pending_aggregated_attestations) > MAX_PENDING_ATTESTATIONS: - self._pending_aggregated_attestations = self._pending_aggregated_attestations[ - -MAX_PENDING_ATTESTATIONS: - ] def _replay_pending_attestations(self) -> None: """Retry buffered attestations after a block is processed.""" @@ -541,7 +540,7 @@ def _replay_pending_attestations(self) -> None: # - B fails: T2 still missing, B is re-appended. # Post-loop queue: [B]. pending = self._pending_attestations - self._pending_attestations = [] + self._pending_attestations = deque(maxlen=MAX_PENDING_ATTESTATIONS) for attestation in pending: try: self.store = self.spec.on_gossip_attestation( @@ -555,7 +554,7 @@ def _replay_pending_attestations(self) -> None: # Same mechanism for aggregated attestations. pending_agg = self._pending_aggregated_attestations - self._pending_aggregated_attestations = [] + self._pending_aggregated_attestations = deque(maxlen=MAX_PENDING_ATTESTATIONS) for signed_attestation in pending_agg: try: self.store = self.spec.on_gossip_aggregated_attestation( diff --git a/tests/lean_spec/subspecs/sync/test_service.py b/tests/lean_spec/subspecs/sync/test_service.py index 754e8d5c6..bd22c225d 100644 --- a/tests/lean_spec/subspecs/sync/test_service.py +++ b/tests/lean_spec/subspecs/sync/test_service.py @@ -437,7 +437,7 @@ async def test_attestation_buffered_when_block_unknown( await sync_service.on_gossip_attestation(attestation) - assert sync_service._pending_attestations == [attestation] + assert list(sync_service._pending_attestations) == [attestation] async def test_buffered_attestation_replayed_after_block( self, @@ -468,7 +468,7 @@ async def test_buffered_attestation_replayed_after_block( await sync_service.on_gossip_block(block, peer_id) # Attestation was replayed (accepted by mock store). - assert sync_service._pending_attestations == [] + assert list(sync_service._pending_attestations) == [] mock_store = cast(MockForkchoiceStore, sync_service.store) assert attestation in mock_store._attestations_received @@ -806,7 +806,7 @@ async def test_aggregated_buffered_on_key_error( mock_store.reject_aggregated_attestation = lambda _att: True await sync_service.on_gossip_aggregated_attestation(signed) - assert sync_service._pending_aggregated_attestations == [signed] + assert list(sync_service._pending_aggregated_attestations) == [signed] async def test_replay_pending_mixed_success_and_failure( self, @@ -827,7 +827,7 @@ async def test_replay_pending_mixed_success_and_failure( sync_service._replay_pending_attestations() assert ok_att in mock_store._attestations_received - assert sync_service._pending_aggregated_attestations == [bad_signed] + assert list(sync_service._pending_aggregated_attestations) == [bad_signed] class TestReplayPendingAttestationsPlain: @@ -856,4 +856,4 @@ def test_replay_plain_mixed_success_and_failure(self, sync_service: SyncService) sync_service._replay_pending_attestations() assert ok_att in mock_store._attestations_received - assert sync_service._pending_attestations == [bad_att] + assert list(sync_service._pending_attestations) == [bad_att]