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
56 changes: 55 additions & 1 deletion src/lean_spec/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
python -m lean_spec --genesis genesis.json --bootnode /ip4/127.0.0.1/tcp/9000
python -m lean_spec --genesis genesis.json --bootnode enr:-IS4QHCYrYZbAKW...
python -m lean_spec --genesis genesis.json --checkpoint-sync-url http://localhost:5052
python -m lean_spec --genesis genesis.json --validator-keys ./keys --node-id node_0

Options:
--genesis Path to genesis JSON file (required)
--bootnode Bootnode address (multiaddr or ENR string, can be repeated)
--listen Address to listen on (default: /ip4/0.0.0.0/tcp/9000)
--checkpoint-sync-url URL to fetch finalized checkpoint state for fast sync
--validator-keys Path to validator keys directory
--node-id Node identifier for validator assignment (default: node_0)
"""

from __future__ import annotations
Expand All @@ -32,6 +35,7 @@
from lean_spec.subspecs.networking.reqresp.message import Status
from lean_spec.subspecs.node import Node, NodeConfig
from lean_spec.subspecs.ssz.hash import hash_tree_root
from lean_spec.subspecs.validator import ValidatorRegistry
from lean_spec.types import Bytes32

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -139,13 +143,15 @@ def create_anchor_block(state: State) -> Block:
def _init_from_genesis(
genesis: GenesisConfig,
event_source: LiveNetworkEventSource,
validator_registry: ValidatorRegistry | None = None,
) -> Node:
"""
Initialize a node from genesis configuration.

Args:
genesis: Genesis configuration with time and validators.
event_source: Network transport for the node.
validator_registry: Optional registry with validator secret keys.

Returns:
A fully initialized Node starting from genesis.
Expand All @@ -165,6 +171,7 @@ def _init_from_genesis(
validators=genesis.to_validators(),
event_source=event_source,
network=event_source.reqresp_client,
validator_registry=validator_registry,
)

# Create and return the node.
Expand All @@ -175,6 +182,7 @@ async def _init_from_checkpoint(
checkpoint_sync_url: str,
genesis: GenesisConfig,
event_source: LiveNetworkEventSource,
validator_registry: ValidatorRegistry | None = None,
) -> Node | None:
"""
Initialize a node from a checkpoint state fetched from a remote node.
Expand All @@ -200,6 +208,7 @@ async def _init_from_checkpoint(
checkpoint_sync_url: URL of the node to fetch checkpoint state from.
genesis: Local genesis configuration for validation.
event_source: Network transport for the node.
validator_registry: Optional registry with validator secret keys.

Returns:
A fully initialized Node if successful, None if checkpoint sync failed.
Expand Down Expand Up @@ -269,6 +278,7 @@ async def _init_from_checkpoint(
validators=state.validators,
event_source=event_source,
network=event_source.reqresp_client,
validator_registry=validator_registry,
)

# Create node and inject checkpoint store.
Expand Down Expand Up @@ -300,6 +310,8 @@ async def run_node(
bootnodes: list[str],
listen_addr: str,
checkpoint_sync_url: str | None = None,
validator_keys_path: Path | None = None,
node_id: str = "node_0",
) -> None:
"""
Run the lean consensus node.
Expand All @@ -309,6 +321,8 @@ async def run_node(
bootnodes: List of bootnode multiaddrs to connect to.
listen_addr: Address to listen on.
checkpoint_sync_url: Optional URL to fetch checkpoint state for fast sync.
validator_keys_path: Optional path to validator keys directory.
node_id: Node identifier for validator assignment.
"""
logger.info("Loading genesis from %s", genesis_path)
genesis = GenesisConfig.from_json_file(genesis_path)
Expand All @@ -318,6 +332,27 @@ async def run_node(
len(genesis.genesis_validators),
)

# Load validator keys if path provided.
#
# The registry holds secret keys for validators assigned to this node.
# Without a registry, the node runs in passive mode (sync only).
validator_registry: ValidatorRegistry | None = None
if validator_keys_path is not None:
validator_registry = ValidatorRegistry.from_json(
node_id=node_id,
validators_path=validator_keys_path / "validators.json",
manifest_path=validator_keys_path / "hash-sig-keys/validator-keys-manifest.json",
)
if len(validator_registry) > 0:
logger.info(
"Loaded %d validators for node %s: indices=%s",
len(validator_registry),
node_id,
validator_registry.indices(),
)
else:
logger.warning("No validators assigned to node %s", node_id)

event_source = LiveNetworkEventSource.create()

# Two initialization paths: checkpoint sync or genesis sync.
Expand All @@ -337,6 +372,7 @@ async def run_node(
checkpoint_sync_url=checkpoint_sync_url,
genesis=genesis,
event_source=event_source,
validator_registry=validator_registry,
)
if node is None:
# Checkpoint sync failed. Exit rather than falling back.
Expand All @@ -345,7 +381,11 @@ async def run_node(
# They explicitly requested checkpoint sync for a reason.
return
else:
node = _init_from_genesis(genesis=genesis, event_source=event_source)
node = _init_from_genesis(
genesis=genesis,
event_source=event_source,
validator_registry=validator_registry,
)

logger.info("Node initialized, peer_id=%s", event_source.connection_manager.peer_id)

Expand Down Expand Up @@ -418,6 +458,18 @@ def main() -> None:
default=None,
help="URL to fetch finalized checkpoint state for fast sync (e.g., http://localhost:5052)",
)
parser.add_argument(
"--validator-keys",
type=Path,
default=None,
help="Path to validator keys directory",
)
parser.add_argument(
"--node-id",
type=str,
default="node_0",
help="Node identifier for validator assignment (default: node_0)",
)
parser.add_argument(
"-v",
"--verbose",
Expand All @@ -436,6 +488,8 @@ def main() -> None:
args.bootnodes,
args.listen,
args.checkpoint_sync_url,
args.validator_keys,
args.node_id,
)
)
except KeyboardInterrupt:
Expand Down
2 changes: 2 additions & 0 deletions src/lean_spec/subspecs/validator/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ async def _maybe_produce_block(self, slot: Slot) -> None:
#
# This adds our attestation and signatures to the block.
signed_block = self._sign_block(block, validator_index, signatures)

self._blocks_produced += 1
metrics.blocks_proposed.inc()

Expand Down Expand Up @@ -261,6 +262,7 @@ async def _produce_attestations(self, slot: Slot) -> None:

# Sign the attestation using our secret key.
signed_attestation = self._sign_attestation(attestation_data, validator_index)

self._attestations_produced += 1
metrics.attestations_produced.inc()

Expand Down
Loading