Skip to content

Commit 7385f8d

Browse files
authored
core: add network client module and main node (leanEthereum#275)
* core: add network client module and main node * cleanup
1 parent dec2290 commit 7385f8d

5 files changed

Lines changed: 778 additions & 26 deletions

File tree

src/lean_spec/__main__.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""
2+
Lean consensus node CLI entry point.
3+
4+
Run a minimal lean consensus client that can sync with other lean consensus nodes.
5+
6+
Usage::
7+
8+
python -m lean_spec --genesis genesis.json --bootnode /ip4/127.0.0.1/tcp/9000
9+
10+
Options:
11+
--genesis Path to genesis JSON file (required)
12+
--bootnode Multiaddr of bootnode to connect to (can be repeated)
13+
--listen Address to listen on (default: /ip4/0.0.0.0/tcp/9000)
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import argparse
19+
import asyncio
20+
import logging
21+
from pathlib import Path
22+
23+
from lean_spec.subspecs.containers import Checkpoint
24+
from lean_spec.subspecs.containers.slot import Slot
25+
from lean_spec.subspecs.genesis import GenesisConfig
26+
from lean_spec.subspecs.networking.client import LiveNetworkEventSource
27+
from lean_spec.subspecs.networking.reqresp.message import Status
28+
from lean_spec.subspecs.node import Node, NodeConfig
29+
from lean_spec.types import Bytes32
30+
31+
logger = logging.getLogger(__name__)
32+
33+
34+
def setup_logging(verbose: bool = False) -> None:
35+
"""Configure logging for the node."""
36+
level = logging.DEBUG if verbose else logging.INFO
37+
logging.basicConfig(
38+
level=level,
39+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
40+
datefmt="%Y-%m-%d %H:%M:%S",
41+
)
42+
43+
44+
async def run_node(
45+
genesis_path: Path,
46+
bootnodes: list[str],
47+
listen_addr: str,
48+
) -> None:
49+
"""
50+
Run the lean consensus node.
51+
52+
Args:
53+
genesis_path: Path to genesis JSON file.
54+
bootnodes: List of bootnode multiaddrs to connect to.
55+
listen_addr: Address to listen on.
56+
"""
57+
# Load genesis configuration.
58+
logger.info("Loading genesis from %s", genesis_path)
59+
genesis = GenesisConfig.from_json_file(genesis_path)
60+
logger.info(
61+
"Genesis loaded: time=%d, validators=%d",
62+
genesis.genesis_time,
63+
len(genesis.genesis_validators),
64+
)
65+
66+
# Create network transport.
67+
event_source = LiveNetworkEventSource.create()
68+
69+
# Create initial status for handshakes.
70+
#
71+
# At genesis, our finalized and head are both the genesis block.
72+
genesis_status = Status(
73+
finalized=Checkpoint(root=Bytes32.zero(), slot=Slot(0)),
74+
head=Checkpoint(root=Bytes32.zero(), slot=Slot(0)),
75+
)
76+
event_source.set_status(genesis_status)
77+
78+
# Create node configuration.
79+
config = NodeConfig(
80+
genesis_time=genesis.genesis_time,
81+
validators=genesis.to_validators(),
82+
event_source=event_source,
83+
network=event_source.reqresp_client,
84+
)
85+
86+
# Create the node.
87+
node = Node.from_genesis(config)
88+
logger.info("Node initialized, peer_id=%s", event_source.connection_manager.peer_id)
89+
90+
# Update status with actual genesis block root.
91+
#
92+
# At genesis, the head and finalized are both the genesis block.
93+
# The store.head is initialized to the genesis block root.
94+
genesis_root = node.store.head
95+
updated_status = Status(
96+
finalized=Checkpoint(root=genesis_root, slot=Slot(0)),
97+
head=Checkpoint(root=genesis_root, slot=Slot(0)),
98+
)
99+
event_source.set_status(updated_status)
100+
101+
# Connect to bootnodes.
102+
for bootnode in bootnodes:
103+
logger.info("Connecting to bootnode %s", bootnode)
104+
peer_id = await event_source.dial(bootnode)
105+
if peer_id:
106+
logger.info("Connected to bootnode, peer_id=%s", peer_id)
107+
else:
108+
logger.warning("Failed to connect to bootnode %s", bootnode)
109+
110+
# Start listening (in background).
111+
if listen_addr:
112+
logger.info("Starting listener on %s", listen_addr)
113+
asyncio.create_task(event_source.listen(listen_addr))
114+
115+
# Run the node.
116+
logger.info("Starting consensus node...")
117+
event_source._running = True
118+
await node.run()
119+
120+
121+
def main() -> None:
122+
"""CLI entry point."""
123+
parser = argparse.ArgumentParser(
124+
description="Lean consensus node",
125+
formatter_class=argparse.RawDescriptionHelpFormatter,
126+
epilog=__doc__,
127+
)
128+
parser.add_argument(
129+
"--genesis",
130+
required=True,
131+
type=Path,
132+
help="Path to genesis JSON file",
133+
)
134+
parser.add_argument(
135+
"--bootnode",
136+
action="append",
137+
default=[],
138+
dest="bootnodes",
139+
help="Bootnode multiaddr (can be repeated)",
140+
)
141+
parser.add_argument(
142+
"--listen",
143+
default="/ip4/0.0.0.0/tcp/9000",
144+
help="Address to listen on (default: /ip4/0.0.0.0/tcp/9000)",
145+
)
146+
parser.add_argument(
147+
"-v",
148+
"--verbose",
149+
action="store_true",
150+
help="Enable debug logging",
151+
)
152+
153+
args = parser.parse_args()
154+
155+
setup_logging(args.verbose)
156+
157+
try:
158+
asyncio.run(run_node(args.genesis, args.bootnodes, args.listen))
159+
except KeyboardInterrupt:
160+
logger.info("Shutting down...")
161+
162+
163+
if __name__ == "__main__":
164+
main()
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Network Client Module.
3+
4+
Bridges the transport layer to the sync service by implementing
5+
the NetworkRequester and NetworkEventSource protocols.
6+
7+
Components
8+
----------
9+
ReqRespClient
10+
Implements NetworkRequester using ConnectionManager.
11+
Handles BlocksByRoot and Status requests.
12+
13+
LiveNetworkEventSource
14+
Implements NetworkEventSource.
15+
Bridges connection events to NetworkService events.
16+
"""
17+
18+
from .event_source import LiveNetworkEventSource
19+
from .reqresp_client import ReqRespClient
20+
21+
__all__ = [
22+
"LiveNetworkEventSource",
23+
"ReqRespClient",
24+
]

0 commit comments

Comments
 (0)