Skip to content

Commit d4c94c7

Browse files
tcoratgerclaude
andauthored
refactor(node/validator): simplify and de-verbose the validator module (leanEthereum#1108)
Clarity and documentation pass over the validator service, registry, and constants. No behavior change; the unit tests pass. - Docs: collapse verbose module/class/method docstrings to one line where the signature already says it, strip the banned Why:/Effect: labels from the constants, and trim step-narration comments and debug logs. Keep the genuine rationale (authenticated network-tip bound, wait-for-block, the local-processing threshold, the gated-slot invariant) glued to its code. - Modern Python: use dataclasses.replace to carry the unchanged signing key; resolve the proposer by direct registry membership instead of a loop that recomputed it; name the attestation dedup horizon as a constant. - Inline the single-use helpers (node-mapping load, secret-key load, the attestation signer) so each flow reads in one place. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ebf596b commit d4c94c7

6 files changed

Lines changed: 158 additions & 588 deletions

File tree

src/lean_spec/node/validator/__init__.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
"""
2-
Validator service module for producing blocks and attestations.
2+
Validator key management and duty execution.
33
44
Validators are the active participants in Ethereum consensus.
5-
This module provides:
6-
7-
- A registry that manages validator secret keys for signing
8-
- A service that drives duty execution based on the slot clock
9-
10-
Lifecycle:
11-
12-
1. Load validator keys from YAML configuration
13-
2. Start the service to monitor slot intervals
14-
3. At interval 0, produce blocks if scheduled
15-
4. At interval 1, produce attestations for non-proposers
5+
A registry holds the signing keys this node controls.
6+
A service runs each validator's duties off the slot clock.
167
"""
178

189
from lean_spec.node.validator.registry import ValidatorRegistry
Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,12 @@
1-
"""
2-
Validator duty-gate thresholds.
3-
4-
Informative, not normative:
5-
6-
- Shape when this node signs.
7-
- Do not change what consensus accepts.
8-
- Clients may diverge without breaking interop.
9-
"""
1+
"""Validator duty-gate thresholds"""
102

113
from typing import Final
124

135
SYNC_LAG_THRESHOLD: Final[int] = 4
14-
"""Slot lag past which the local view is too stale to sign.
15-
16-
Why:
17-
We justify and finalize within a handful of slots.
18-
A 4-slot lag is one full justification window behind real time.
19-
A vote from that view lands on a subtree the network has left.
20-
"""
6+
"""Slot lag past which the local view is too stale to sign."""
217

228
NETWORK_STALL_THRESHOLD: Final[int] = 8
23-
"""Slot lag past which the whole network is treated as stalled.
24-
25-
Why:
26-
Set to twice the local threshold (8 = 2 * 4).
27-
Ordinary jitter at the local boundary must not trip this branch.
28-
29-
Effect:
30-
Even the freshest locally validated block is 8 slots behind.
31-
The cause is a streak of skipped proposals, not this node lagging.
32-
Duties stay live so the chain can advance through the gap.
33-
"""
9+
"""Slot lag treated as a network-wide stall, so duties stay live (twice the local gate)."""
3410

3511
HYSTERESIS_BAND: Final[int] = 2
36-
"""Slot band that holds the gate closed near the threshold.
37-
38-
Why:
39-
Without a band a single late gossip block flips the decision.
40-
Slot-over-slot flips would stutter the attestation stream.
41-
42-
Effect:
43-
Once closed, the gate reopens only when lag drops to 4 - 2 = 2.
44-
"""
12+
"""Slot band holding the gate closed near the threshold, so it cannot flip slot-to-slot."""

src/lean_spec/node/validator/registry.py

Lines changed: 38 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
11
"""
2-
Validator registry for managing validator keys.
2+
Validator key loading.
33
4-
Loads validator keys from YAML configuration files compatible with ream and zeam.
4+
Two YAML files describe the keys.
55
6-
The registry supports two YAML files:
6+
- validators.yaml maps each node to the validator indices it controls:
77
8-
1. **validators.yaml** - Maps node IDs to validator indices:
8+
lean_spec_0:
9+
- 0
10+
- 1
11+
lean_spec_1:
12+
- 2
913
10-
lean_spec_0:
11-
- 0
12-
- 1
13-
lean_spec_1:
14-
- 2
14+
- validator-keys-manifest.yaml lists each validator's key metadata and file paths:
1515
16-
2. **validator-keys-manifest.yaml** - Contains key metadata and file paths:
17-
18-
key_scheme: SIGTopLevelTargetSumLifetime32Dim64Base8
19-
hash_function: Poseidon
20-
num_validators: 3
21-
validators:
22-
- index: 0
23-
public_key_hex: 0xe2a03c...
24-
private_key_file: validator_0_secret_key.ssz
16+
key_scheme: SIGTopLevelTargetSumLifetime32Dim64Base8
17+
hash_function: Poseidon
18+
num_validators: 3
19+
validators:
20+
- index: 0
21+
public_key_hex: 0xe2a03c...
22+
private_key_file: validator_0_secret_key.ssz
2523
"""
2624

2725
from __future__ import annotations
@@ -39,9 +37,6 @@
3937

4038
logger = logging.getLogger(__name__)
4139

42-
type NodeValidatorMapping = dict[str, list[int]]
43-
"""Mapping from node identifier to list of validator indices."""
44-
4540

4641
class ValidatorManifestEntry(BaseModel):
4742
"""Single validator entry from the manifest file."""
@@ -63,12 +58,7 @@ class ValidatorManifestEntry(BaseModel):
6358

6459

6560
class ValidatorManifest(BaseModel):
66-
"""
67-
Key metadata from validator-keys-manifest.yaml.
68-
69-
Contains cryptographic scheme info and validator key paths.
70-
This format matches ream's manifest structure.
71-
"""
61+
"""Key metadata for every validator, matching the ream manifest format."""
7262

7363
key_scheme: str
7464
"""Signature scheme identifier (e.g., SIGTopLevelTargetSumLifetime32Dim64Base8)."""
@@ -96,46 +86,18 @@ class ValidatorManifest(BaseModel):
9686

9787
@classmethod
9888
def from_yaml_file(cls, path: Path) -> ValidatorManifest:
99-
"""
100-
Load manifest from YAML file.
101-
102-
Args:
103-
path: Path to validator-keys-manifest.yaml.
104-
105-
Returns:
106-
Validated ValidatorManifest instance.
107-
"""
89+
"""Load and validate a manifest from a YAML file."""
10890
with path.open() as f:
10991
return cls.model_validate(yaml.safe_load(f))
11092

11193

112-
def load_node_validator_mapping(path: Path) -> NodeValidatorMapping:
113-
"""
114-
Load node-to-validator index mapping from validators.yaml.
115-
116-
Maps node identifiers to lists of validator indices they control.
117-
118-
Args:
119-
path: Path to validators.yaml.
120-
121-
Returns:
122-
Mapping from node ID to list of validator indices.
123-
Empty dict if file is empty.
124-
"""
125-
with path.open() as yaml_file:
126-
parsed_yaml = yaml.safe_load(yaml_file)
127-
# YAML returns None for empty file
128-
return parsed_yaml or {}
129-
130-
13194
@dataclass(frozen=True, slots=True)
13295
class ValidatorEntry:
13396
"""
13497
A single validator's key material.
13598
136-
Holds the index and both secret keys needed for signing.
137-
Attestation and proposal keys are separate to allow independent
138-
OTS signing within the same slot.
99+
Attestation and proposal keys are separate.
100+
This lets one validator sign both within the same slot without OTS conflict.
139101
"""
140102

141103
index: ValidatorIndex
@@ -150,63 +112,33 @@ class ValidatorEntry:
150112

151113
@dataclass(slots=True)
152114
class ValidatorRegistry:
153-
"""
154-
Registry of validator keys controlled by this node.
155-
156-
The registry holds secret keys for validators assigned to this node.
157-
It provides lookup by validator index for signing operations.
158-
"""
115+
"""Signing keys for the validators this node controls."""
159116

160117
_validators: dict[ValidatorIndex, ValidatorEntry] = field(default_factory=dict)
161118
"""Map from validator index to entry."""
162119

163120
def add(self, entry: ValidatorEntry) -> None:
164-
"""
165-
Add or replace a validator entry in the registry.
166-
167-
Replaces any existing entry with the same index.
168-
Used to persist updated key state after signing.
169-
170-
Args:
171-
entry: Validator entry to add.
172-
"""
121+
"""Add a validator entry, replacing any existing entry with the same index."""
173122
self._validators[entry.index] = entry
174123

175124
def get(self, index: ValidatorIndex) -> ValidatorEntry | None:
176-
"""
177-
Get validator entry by index.
178-
179-
Args:
180-
index: Validator index to look up.
181-
182-
Returns:
183-
Validator entry if found, None otherwise.
184-
"""
125+
"""Return the validator entry for an index, or None if not controlled."""
185126
return self._validators.get(index)
186127

187128
def __contains__(self, index: ValidatorIndex) -> bool:
188129
"""Check if we control this validator."""
189130
return index in self._validators
190131

191132
def indices(self) -> ValidatorIndices:
192-
"""
193-
Get all validator indices we control.
194-
195-
Returns:
196-
ValidatorIndices collection.
197-
"""
133+
"""Return every validator index this node controls."""
198134
return ValidatorIndices(data=list(self._validators.keys()))
199135

200136
def primary_index(self) -> ValidatorIndex | None:
201137
"""
202-
Get the primary validator index for store-level identity.
138+
The store-level identity for this node, or None if it controls no validators.
203139
204-
Returns the first validator index in the registry.
205-
With ATTESTATION_COMMITTEE_COUNT = 1, all validators share subnet 0,
206-
so a single ID suffices for store-level operations.
207-
208-
Returns:
209-
First validator index, or None if registry is empty.
140+
Every validator shares the single attestation subnet.
141+
So the first controlled index suffices for store-level operations.
210142
"""
211143
if not self._validators:
212144
return None
@@ -219,24 +151,13 @@ def __len__(self) -> int:
219151
@classmethod
220152
def from_keys_directory(cls, node_id: str, base_directory: Path | str) -> ValidatorRegistry:
221153
"""
222-
Load a validator registry from the ream/zeam keystore layout.
223-
224-
Two files relative to the base directory:
225-
226-
- validators.yaml: maps each node to its validator indices.
227-
- hash-sig-keys/validator-keys-manifest.yaml: lists each validator's
228-
key metadata and SSZ file path.
154+
Load a registry from the ream/zeam keystore layout.
229155
230-
Args:
231-
node_id: Identifier looked up in the node-to-validator mapping.
232-
base_directory: Directory containing the two layout files.
156+
Reads validators.yaml and hash-sig-keys/validator-keys-manifest.yaml,
157+
both relative to the base directory.
233158
234-
Returns:
235-
Registry populated with the keys assigned to the node.
236-
237-
Raises:
238-
FileNotFoundError: If the manifest file is missing.
239-
A missing validators mapping is allowed and yields an empty registry.
159+
A missing manifest raises FileNotFoundError.
160+
A missing validators mapping is allowed and yields an empty registry.
240161
"""
241162
base = Path(base_directory)
242163
manifest_path = base / "hash-sig-keys" / "validator-keys-manifest.yaml"
@@ -255,28 +176,13 @@ def from_yaml(
255176
validators_path: Path | str,
256177
manifest_path: Path | str,
257178
) -> ValidatorRegistry:
258-
"""
259-
Load validator registry from YAML configuration files.
260-
261-
Loading process:
262-
263-
1. Read validators.yaml to find indices assigned to this node
264-
2. Read manifest to get key file paths
265-
3. Load secret keys from SSZ files
266-
267-
Args:
268-
node_id: Identifier for this node in validators.yaml.
269-
validators_path: Path to validators.yaml.
270-
manifest_path: Path to validator-keys-manifest.yaml.
271-
272-
Returns:
273-
Registry populated with validator keys for this node.
274-
"""
179+
"""Load a registry for one node from its validators.yaml and manifest files."""
275180
validators_path = Path(validators_path)
276181
manifest_path = Path(manifest_path)
277182

278-
# Load node-to-validator mapping.
279-
node_mapping = load_node_validator_mapping(validators_path)
183+
# Read the node-to-validator mapping; an empty file parses to None.
184+
with validators_path.open() as validators_file:
185+
node_mapping = yaml.safe_load(validators_file) or {}
280186

281187
# Get indices assigned to this node.
282188
assigned_indices = node_mapping.get(node_id, [])
@@ -308,7 +214,7 @@ def from_yaml(
308214
)
309215
continue
310216

311-
# Load attestation secret key from SSZ file.
217+
# Decode the attestation key from its SSZ file.
312218
attestation_key_path = manifest_directory / manifest_entry.attestation_private_key_file
313219
try:
314220
attestation_secret_key = SecretKey.decode_bytes(attestation_key_path.read_bytes())
@@ -321,7 +227,7 @@ def from_yaml(
321227
f"Failed to load attestation key for validator {validator_index}: {exception}"
322228
) from exception
323229

324-
# Load proposal secret key from SSZ file.
230+
# Decode the proposal key from its SSZ file.
325231
proposal_key_path = manifest_directory / manifest_entry.proposal_private_key_file
326232
try:
327233
proposal_secret_key = SecretKey.decode_bytes(proposal_key_path.read_bytes())

0 commit comments

Comments
 (0)