Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .claude/agents/doc-writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,41 @@ def verify_block(self, block: Block) -> bool:
"""
```

### Use bullet points or enumeration for lists

When listing multiple items, use structured formatting. Helps readers maintain focus.

**Bad** - Inline list, hard to scan:
```python
"""
The verification checks structural validity, cryptographic correctness,
and state transition rules before accepting the block.
"""
```

**Good** - Bullet points:
```python
"""
The verification checks:

- Structural validity
- Cryptographic correctness
- State transition rules
"""
```

**Good** - Numbered steps for sequential operations:
```python
"""
Processing proceeds in order:

1. Validate input format
2. Check signatures
3. Apply state transition
4. Update forkchoice
"""
```

## Project-Specific Requirements

- Line length: 100 characters maximum
Expand Down
94 changes: 93 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,99 @@ uvx tox # Everything (checks + tests + docs)
- Google docstring style
- Test files/functions must start with `test_`
- **No example code in docstrings**: Do not include `Example:` sections with code blocks in docstrings. Keep documentation concise and focused on explaining *what* and *why*, not *how to use*. Unit tests serve as usage examples.
- **Avoid explicit function names in documentation**: In docstrings and comments, describe behavior using plain language rather than explicit function or method names. Names change over time, making documentation stale. Prefer descriptive sentences like "tick the store forward" instead of referencing the exact API signature.

### Documentation Rules (CRITICAL)

**NEVER use explicit function or method names in documentation.**

Names change. Documentation becomes stale. Use plain language instead.

Bad:
```python
# The shutdown task waits for stop() to be called, then signals
# all services to terminate. Once all services exit, TaskGroup completes.
```

Good:
```python
# A separate task monitors the shutdown signal.
# When triggered, it stops all services.
# Once services exit, execution completes.
```

**Write short, scannable sentences.**

Attention spans are short. Capture the reader precisely and concisely.

- One idea per line.
- Add blank lines between logical groups.
- Avoid long compound sentences.

Bad:
```python
# The state includes initial checkpoints, validator registry,
# and configuration derived from genesis time.
```

Good:
```python
# Includes initial checkpoints, validator registry, and config.
```

**Use bullet points or enumeration for lists.**

When listing multiple items, use structured formatting. Helps readers maintain focus.

Bad:
```python
"""
The verification checks structural validity, cryptographic correctness,
and state transition rules before accepting the block.
"""
```

Good:
```python
"""
The verification checks:

- Structural validity
- Cryptographic correctness
- State transition rules
"""
```

Or with numbered steps:
```python
"""
Processing proceeds in order:

1. Validate input format
2. Check signatures
3. Apply state transition
4. Update forkchoice
"""
```

Bad:
```python
"""
Wait for shutdown signal then stop services.

This task runs alongside the services. When shutdown is signaled,
it stops both services, allowing their run loops to exit gracefully.
"""
```

Good:
```python
"""
Wait for shutdown signal then stop services.

Runs alongside the services.
When shutdown is signaled, stops all services gracefully.
"""
```

## Test Framework Structure

Expand Down
5 changes: 5 additions & 0 deletions src/lean_spec/subspecs/node/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Node orchestrator for the Lean Ethereum consensus client."""

from .node import Node, NodeConfig

__all__ = ["Node", "NodeConfig"]
212 changes: 212 additions & 0 deletions src/lean_spec/subspecs/node/node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""
Consensus node orchestrator.

Wires together all services and runs them with structured concurrency.

The Node is the top-level entry point for a minimal Ethereum consensus client.
It initializes all components from genesis configuration and coordinates their
concurrent execution.
"""

from __future__ import annotations

import asyncio
import signal
import time
from collections.abc import Callable
from dataclasses import dataclass, field

from lean_spec.subspecs.chain import ChainService, SlotClock
from lean_spec.subspecs.containers import Block, BlockBody, State
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.networking import NetworkEventSource, NetworkService
from lean_spec.subspecs.ssz.hash import hash_tree_root
from lean_spec.subspecs.sync import BlockCache, NetworkRequester, PeerManager, SyncService
from lean_spec.types import Bytes32, Uint64


@dataclass(frozen=True, slots=True)
class NodeConfig:
"""
Configuration for a consensus node.

Provides all parameters needed to initialize a node from genesis.
"""

genesis_time: Uint64
"""Unix timestamp when slot 0 begins."""

validators: Validators
"""Initial validator set for genesis state."""

event_source: NetworkEventSource
"""Source of network events."""

network: NetworkRequester
"""Interface for requesting blocks from peers."""

time_fn: Callable[[], float] = field(default=time.time)
"""Time source (injectable for deterministic testing)."""


@dataclass(slots=True)
class Node:
"""
Consensus node orchestrator.

Initializes all services from genesis.
Runs them concurrently with structured concurrency.
"""

store: Store
"""Forkchoice store containing chain state."""

clock: SlotClock
"""Slot clock for time conversion."""

sync_service: SyncService
"""Sync service that coordinates state updates."""

chain_service: ChainService
"""Chain service that drives the consensus clock."""

network_service: NetworkService
"""Network service that routes events to sync."""

_shutdown: asyncio.Event = field(default_factory=asyncio.Event)
"""Event signaling shutdown request."""

@classmethod
def from_genesis(cls, config: NodeConfig) -> Node:
"""
Create a fully-wired node from genesis configuration.

Args:
config: Node configuration with genesis parameters.

Returns:
A Node ready to run.
"""
# Generate genesis state from validators.
#
# Includes initial checkpoints, validator registry, and config.
state = State.generate_genesis(config.genesis_time, config.validators)

# Create genesis block.
#
# Slot 0, no parent, empty body.
# State root is the hash of the genesis state.
block = Block(
slot=Slot(0),
proposer_index=Uint64(0),
parent_root=Bytes32.zero(),
state_root=hash_tree_root(state),
body=BlockBody(attestations=AggregatedAttestations(data=[])),
)

# Initialize forkchoice store.
#
# Genesis block is both justified and finalized.
store = Store.get_forkchoice_store(state, block)

# Create shared dependencies.
clock = SlotClock(genesis_time=config.genesis_time, _time_fn=config.time_fn)
peer_manager = PeerManager()
block_cache = BlockCache()

# Wire services together.
#
# Sync service is the hub. It owns the store and coordinates updates.
# Chain and network services communicate through it.
sync_service = SyncService(
store=store,
peer_manager=peer_manager,
block_cache=block_cache,
clock=clock,
network=config.network,
)

chain_service = ChainService(sync_service=sync_service, clock=clock)
network_service = NetworkService(
sync_service=sync_service,
event_source=config.event_source,
)

return cls(
store=store,
clock=clock,
sync_service=sync_service,
chain_service=chain_service,
network_service=network_service,
)

async def run(self, *, install_signal_handlers: bool = True) -> None:
"""
Run all services until shutdown.

Returns when shutdown is requested or a service fails.

Args:
install_signal_handlers: Whether to handle SIGINT/SIGTERM.
Disable for testing or non-main threads.
"""
if install_signal_handlers:
self._install_signal_handlers()

# Run services concurrently.
#
# A separate task monitors the shutdown signal.
# When triggered, it stops all services.
# Once services exit, execution completes.
async with asyncio.TaskGroup() as tg:
tg.create_task(self.chain_service.run())
tg.create_task(self.network_service.run())
tg.create_task(self._wait_shutdown())

def _install_signal_handlers(self) -> None:
"""
Install signal handlers for graceful shutdown.

Handles SIGINT (Ctrl+C) and SIGTERM (process termination).

Silently ignores errors if handlers cannot be installed.
This happens in non-main threads or embedded contexts.
"""
try:
loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, self._shutdown.set)
except (ValueError, RuntimeError):
# Cannot add handlers outside main thread.
pass

async def _wait_shutdown(self) -> None:
"""
Wait for shutdown signal then stop services.

Runs alongside the services.
When shutdown is signaled, stops all services gracefully.
"""
await self._shutdown.wait()

# Signal services to stop.
#
# Each service exits its run loop when stopped.
self.chain_service.stop()
self.network_service.stop()

def stop(self) -> None:
"""
Request graceful shutdown.

Signals the node to stop all services and exit.
"""
self._shutdown.set()

@property
def is_running(self) -> bool:
"""Check if node is currently running."""
return not self._shutdown.is_set()
Loading
Loading