Skip to content

Commit 9ec2177

Browse files
authored
networking: add a network service to route events to sync service (leanEthereum#269)
* chain: add ChainService and tests * networking: add a network service to route events to sync service
1 parent 8a8b572 commit 9ec2177

7 files changed

Lines changed: 1005 additions & 0 deletions

File tree

.claude/agents/code-tester.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ For SSZ types, always test:
148148
6. **Clean Linting**: Must pass `ruff check` and `ruff format`
149149
7. **Type Safety**: All functions must have complete type annotations
150150

151+
## Meaningful Assertions (Critical)
152+
153+
**Fewer tests with strong assertions are better than many tests with trivial assertions.**
154+
155+
- **Don't test**: Internal counters, flags, or that code "ran without error"
156+
- **Do test**: Real state changes, data transformations, and business logic outcomes
157+
- When testing services or routing layers, verify the downstream effect on actual system state
158+
- If a test would pass even when the core logic is broken, the test is worthless
159+
151160
## Decision Framework
152161

153162
When uncertain about test design:

src/lean_spec/subspecs/networking/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@
2222
decode_request,
2323
encode_request,
2424
)
25+
from .service import (
26+
GossipAttestationEvent,
27+
GossipBlockEvent,
28+
NetworkEvent,
29+
NetworkEventSource,
30+
NetworkService,
31+
PeerConnectedEvent,
32+
PeerDisconnectedEvent,
33+
PeerStatusEvent,
34+
)
2535
from .types import DomainType, ForkDigest, ProtocolId
2636

2737
__all__ = [
@@ -48,6 +58,15 @@
4858
"ResponseCode",
4959
"encode_request",
5060
"decode_request",
61+
# Service
62+
"GossipAttestationEvent",
63+
"GossipBlockEvent",
64+
"NetworkEvent",
65+
"NetworkEventSource",
66+
"NetworkService",
67+
"PeerConnectedEvent",
68+
"PeerDisconnectedEvent",
69+
"PeerStatusEvent",
5170
# Types
5271
"DomainType",
5372
"ProtocolId",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Network service module.
3+
4+
This module provides the event routing layer between libp2p and consensus.
5+
"""
6+
7+
from .events import (
8+
GossipAttestationEvent,
9+
GossipBlockEvent,
10+
NetworkEvent,
11+
NetworkEventSource,
12+
PeerConnectedEvent,
13+
PeerDisconnectedEvent,
14+
PeerStatusEvent,
15+
)
16+
from .service import NetworkService
17+
18+
__all__ = [
19+
# Service
20+
"NetworkService",
21+
# Protocol
22+
"NetworkEventSource",
23+
# Events
24+
"GossipAttestationEvent",
25+
"GossipBlockEvent",
26+
"NetworkEvent",
27+
"PeerConnectedEvent",
28+
"PeerDisconnectedEvent",
29+
"PeerStatusEvent",
30+
]
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""
2+
Network Event Types and Source Protocol.
3+
4+
This module defines the event types that flow from the network layer to the
5+
sync service, plus the abstract protocol that event sources must implement.
6+
7+
Event Flow
8+
----------
9+
The network layer (libp2p or test mock) produces events as an async stream.
10+
The network service consumes these events and routes them to sync handlers.
11+
12+
::
13+
14+
Event Source (async iterator)
15+
|
16+
Network Service (pattern matching dispatch)
17+
|
18+
+-- Gossip block events --> Sync block handler
19+
+-- Gossip attestation events --> Sync attestation handler
20+
+-- Peer status events --> Sync peer tracker
21+
"""
22+
23+
from __future__ import annotations
24+
25+
from dataclasses import dataclass
26+
from typing import Protocol, runtime_checkable
27+
28+
from lean_spec.subspecs.containers import SignedBlockWithAttestation
29+
from lean_spec.subspecs.containers.attestation import SignedAttestation
30+
from lean_spec.subspecs.networking.gossipsub.topic import GossipTopic
31+
from lean_spec.subspecs.networking.reqresp.message import Status
32+
from lean_spec.subspecs.networking.types import PeerId
33+
34+
35+
@dataclass(frozen=True, slots=True)
36+
class GossipBlockEvent:
37+
"""
38+
Block received via gossip subscription.
39+
40+
Fired when a signed block arrives from the gossipsub network.
41+
The block may or may not have a known parent in the store.
42+
"""
43+
44+
block: SignedBlockWithAttestation
45+
"""The signed block with attestation proof."""
46+
47+
peer_id: PeerId
48+
"""Peer that propagated this block to us."""
49+
50+
topic: GossipTopic
51+
"""Topic the block was received on (includes fork digest)."""
52+
53+
54+
@dataclass(frozen=True, slots=True)
55+
class GossipAttestationEvent:
56+
"""
57+
Attestation received via gossip subscription.
58+
59+
Fired when a signed attestation arrives from the gossipsub network.
60+
"""
61+
62+
attestation: SignedAttestation
63+
"""The signed attestation."""
64+
65+
peer_id: PeerId
66+
"""Peer that propagated this attestation to us."""
67+
68+
topic: GossipTopic
69+
"""Topic the attestation was received on (includes fork digest)."""
70+
71+
72+
@dataclass(frozen=True, slots=True)
73+
class PeerStatusEvent:
74+
"""
75+
Peer sent their chain status.
76+
77+
Fired when a peer responds to or initiates a Status request.
78+
Contains the peer's view of the chain (head, finalized checkpoint).
79+
"""
80+
81+
peer_id: PeerId
82+
"""Peer that sent their status."""
83+
84+
status: Status
85+
"""The peer's chain status (finalized checkpoint and head)."""
86+
87+
88+
@dataclass(frozen=True, slots=True)
89+
class PeerConnectedEvent:
90+
"""
91+
New peer connection established.
92+
93+
Fired when the transport layer establishes a new connection.
94+
The peer has not yet exchanged Status messages at this point.
95+
"""
96+
97+
peer_id: PeerId
98+
"""Peer that connected."""
99+
100+
101+
@dataclass(frozen=True, slots=True)
102+
class PeerDisconnectedEvent:
103+
"""
104+
Peer disconnected.
105+
106+
Fired when a peer connection is closed:
107+
- either gracefully (Goodbye message) or
108+
- due to transport failure.
109+
"""
110+
111+
peer_id: PeerId
112+
"""Peer that disconnected."""
113+
114+
115+
NetworkEvent = (
116+
GossipBlockEvent
117+
| GossipAttestationEvent
118+
| PeerStatusEvent
119+
| PeerConnectedEvent
120+
| PeerDisconnectedEvent
121+
)
122+
"""Union of all network event types for pattern matching dispatch."""
123+
124+
125+
@runtime_checkable
126+
class NetworkEventSource(Protocol):
127+
"""
128+
Abstract source of network events.
129+
130+
This protocol defines the interface that network implementations must
131+
provide. It is an async iterator that yields NetworkEvent objects.
132+
133+
Any class that implements async iteration over NetworkEvent can serve
134+
as a source.
135+
136+
Usage
137+
-----
138+
::
139+
140+
async for event in event_source:
141+
await handle_event(event)
142+
143+
The source controls backpressure. When the consumer is slow, the
144+
source naturally pauses due to async iteration semantics.
145+
"""
146+
147+
def __aiter__(self) -> NetworkEventSource:
148+
"""Return self as async iterator."""
149+
...
150+
151+
async def __anext__(self) -> NetworkEvent:
152+
"""
153+
Yield the next network event.
154+
155+
Blocks until an event is available.
156+
157+
Returns:
158+
Next event from the network.
159+
160+
Raises:
161+
StopAsyncIteration: When no more events will arrive.
162+
"""
163+
...

0 commit comments

Comments
 (0)