Skip to content

Commit 33ab361

Browse files
authored
genesis: add genesis config (leanEthereum#271)
* chain: add ChainService and tests * networking: add a network service to route events to sync service * node: add consensus node orchestrator * fine tune claude instructions * genesis: add genesis config * small fix
1 parent fa73fb5 commit 33ab361

5 files changed

Lines changed: 363 additions & 0 deletions

File tree

src/lean_spec/subspecs/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
"""Subspecifications for the Lean Ethereum Python specifications."""
2+
3+
from .genesis import GenesisConfig
4+
5+
__all__ = ["GenesisConfig"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Genesis configuration and state initialization."""
2+
3+
from .config import GenesisConfig
4+
5+
__all__ = ["GenesisConfig"]
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Genesis configuration loader."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
from pathlib import Path
7+
8+
from pydantic import Field, field_validator
9+
10+
from lean_spec.subspecs.containers import State, Validator
11+
from lean_spec.subspecs.containers.state import Validators
12+
from lean_spec.types import Bytes52, StrictBaseModel, Uint64
13+
14+
15+
class GenesisConfig(StrictBaseModel):
16+
"""
17+
Configuration that establishes the birth of an Ethereum consensus chain.
18+
19+
Genesis is the shared starting point for all participants in the network.
20+
Without a common genesis, nodes cannot agree on the chain's history.
21+
Every block traces its ancestry back to this origin.
22+
23+
The genesis configuration solves two fundamental coordination problems:
24+
25+
1. **Time Synchronization**: All nodes must agree on when slots begin.
26+
The genesis time anchors the chain's internal clock to real-world time.
27+
From this moment, slots tick forward at fixed intervals. A node can
28+
compute "what slot is it now?" by measuring seconds since genesis.
29+
30+
2. **Initial Trust**: Proof-of-stake requires an initial set of validators.
31+
These validators form the first committee that can produce and attest
32+
to blocks. Without them, no blocks could ever be finalized.
33+
34+
The genesis block (slot 0) is implicit. It has no parent, no proposer,
35+
and no attestations. The first real block builds on top of this implicit
36+
origin, establishing the chain's cryptographic lineage.
37+
38+
Example JSON configuration:
39+
40+
{
41+
"GENESIS_TIME": 1704085200,
42+
"GENESIS_VALIDATORS": [
43+
"0xe2a03c1689769ae5f5762222b170b4a925f3f8e89340ed1cd31d31c134b0abc2...",
44+
"0x0767e659c1b61d30f65eadb7a309c4183d5d4c0f99e935737b89ce95dd1c4568..."
45+
]
46+
}
47+
48+
Field names use UPPERCASE to match the cross-client JSON convention.
49+
Pydantic aliases map them to snake_case Python attributes.
50+
"""
51+
52+
genesis_time: Uint64 = Field(alias="GENESIS_TIME")
53+
"""
54+
Unix timestamp (seconds since 1970-01-01 UTC) when slot 0 begins.
55+
56+
Anchors the chain's clock to real-world time.
57+
58+
Nodes compute the current slot as: (now - genesis_time) / slot_duration.
59+
60+
Immutable once the chain launches.
61+
"""
62+
63+
genesis_validators: list[Bytes52] = Field(alias="GENESIS_VALIDATORS")
64+
"""
65+
Public keys of validators trusted to secure the chain from slot 0.
66+
67+
Bootstrap the proof-of-stake mechanism.
68+
69+
These validators can:
70+
71+
- Propose the first blocks
72+
- Cast attestations for justification/finalization
73+
- Form the supermajority needed for consensus
74+
75+
Each key is 52 bytes (XMSS format).
76+
77+
Security note: 2/3+ collusion controls the chain until new validators join.
78+
"""
79+
80+
@field_validator("genesis_validators", mode="before")
81+
@classmethod
82+
def parse_hex_pubkeys(cls, v: list[str]) -> list[Bytes52]:
83+
"""
84+
Convert hex strings to validated Bytes52 pubkeys.
85+
86+
The JSON contains string representations.
87+
We parse them into typed Bytes52 objects for validation and use.
88+
89+
Args:
90+
v: List of hex-encoded pubkey strings from JSON.
91+
92+
Returns:
93+
List of validated Bytes52 pubkey objects.
94+
"""
95+
return [Bytes52(pk) for pk in v]
96+
97+
def to_validators(self) -> Validators:
98+
"""
99+
Build the genesis validator set with assigned indices.
100+
101+
Each validator needs an index for the registry.
102+
Indices are assigned sequentially starting from 0.
103+
104+
Returns:
105+
Validators container ready for State creation.
106+
"""
107+
return Validators(
108+
data=[
109+
Validator(pubkey=pk, index=Uint64(i))
110+
for i, pk in enumerate(self.genesis_validators)
111+
]
112+
)
113+
114+
def create_state(self) -> State:
115+
"""
116+
Generate the complete genesis state from this configuration.
117+
118+
Combines genesis time and validator set to create the initial
119+
consensus state. This state becomes slot 0 for the chain.
120+
121+
Returns:
122+
Fully initialized genesis State object.
123+
"""
124+
return State.generate_genesis(self.genesis_time, self.to_validators())
125+
126+
@classmethod
127+
def from_json_file(cls, path: Path | str) -> GenesisConfig:
128+
"""
129+
Load configuration from a JSON file on disk.
130+
131+
Use this to load shared genesis files distributed to all clients.
132+
133+
Args:
134+
path: Path to genesis JSON file.
135+
136+
Returns:
137+
Validated GenesisConfig instance.
138+
"""
139+
with open(path) as f:
140+
data = json.load(f)
141+
return cls.model_validate(data)
142+
143+
@classmethod
144+
def from_json(cls, content: str) -> GenesisConfig:
145+
"""
146+
Load configuration from a JSON string.
147+
148+
Use this for testing or programmatic config generation.
149+
150+
Args:
151+
content: JSON content as a string.
152+
153+
Returns:
154+
Validated GenesisConfig instance.
155+
"""
156+
return cls.model_validate_json(content)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for the genesis configuration module."""
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""Tests for the GenesisConfig class."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import tempfile
7+
8+
import pytest
9+
from pydantic import ValidationError
10+
11+
from lean_spec.subspecs.containers.slot import Slot
12+
from lean_spec.subspecs.genesis import GenesisConfig
13+
from lean_spec.types import Bytes52, SSZValueError, Uint64
14+
15+
# Sample pubkeys (52 bytes each, hex-encoded)
16+
SAMPLE_PUBKEY_1 = "0x" + "00" * 52
17+
SAMPLE_PUBKEY_2 = "0x" + "01" * 52
18+
SAMPLE_PUBKEY_3 = "0x" + "02" * 52
19+
20+
SAMPLE_JSON = json.dumps(
21+
{
22+
"GENESIS_TIME": 1704085200,
23+
"GENESIS_VALIDATORS": [SAMPLE_PUBKEY_1, SAMPLE_PUBKEY_2, SAMPLE_PUBKEY_3],
24+
}
25+
)
26+
27+
# Real pubkeys from ream config (split for line length)
28+
REAM_PUBKEY_1 = (
29+
"0xe2a03c16122c7e0f940e2301aa460c54a2e1e8343968bb2782f26636f051e65e"
30+
"c589c858b9c7980b276ebe550056b23f0bdc3b5a"
31+
)
32+
REAM_PUBKEY_2 = (
33+
"0x0767e65924063f79ae92ee1953685f06718b1756cc665a299bd61b4b82055e37"
34+
"7237595d9a27887421b5233d09a50832db2f303d"
35+
)
36+
REAM_PUBKEY_3 = (
37+
"0xd4355005bc37f76f390dcd2bcc51677d8c6ab44e0cc64913fb84ad459789a311"
38+
"05bd9a69afd2690ffd737d22ec6e3b31d47a642f"
39+
)
40+
41+
42+
class TestGenesisConfigJsonLoading:
43+
"""Tests for JSON loading functionality."""
44+
45+
def test_load_from_json_string(self) -> None:
46+
"""Parses JSON with UPPERCASE keys."""
47+
config = GenesisConfig.from_json(SAMPLE_JSON)
48+
49+
assert config.genesis_time == Uint64(1704085200)
50+
assert len(config.genesis_validators) == 3
51+
52+
def test_load_from_json_file(self) -> None:
53+
"""Loads config from file path."""
54+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
55+
f.write(SAMPLE_JSON)
56+
f.flush()
57+
58+
config = GenesisConfig.from_json_file(f.name)
59+
60+
assert config.genesis_time == Uint64(1704085200)
61+
assert len(config.genesis_validators) == 3
62+
63+
def test_pubkeys_parsed_correctly(self) -> None:
64+
"""Pubkeys are converted to Bytes52 instances."""
65+
config = GenesisConfig.from_json(SAMPLE_JSON)
66+
67+
for pk in config.genesis_validators:
68+
assert isinstance(pk, Bytes52)
69+
assert len(pk) == 52
70+
71+
def test_pubkey_without_0x_prefix(self) -> None:
72+
"""Handles pubkeys without 0x prefix (zeam format)."""
73+
json_content = json.dumps(
74+
{
75+
"GENESIS_TIME": 1704085200,
76+
"GENESIS_VALIDATORS": ["00" * 52, "01" * 52],
77+
}
78+
)
79+
config = GenesisConfig.from_json(json_content)
80+
81+
assert len(config.genesis_validators) == 2
82+
assert config.genesis_validators[0] == Bytes52(b"\x00" * 52)
83+
84+
85+
class TestGenesisConfigValidators:
86+
"""Tests for validator conversion."""
87+
88+
def test_to_validators_creates_indexed_list(self) -> None:
89+
"""Validators have correct indices."""
90+
config = GenesisConfig.from_json(SAMPLE_JSON)
91+
validators = config.to_validators()
92+
93+
assert len(validators.data) == 3
94+
95+
for i, validator in enumerate(validators.data):
96+
assert validator.index == Uint64(i)
97+
assert validator.pubkey == config.genesis_validators[i]
98+
99+
def test_empty_validators_list(self) -> None:
100+
"""Handles empty validator list."""
101+
json_content = json.dumps(
102+
{
103+
"GENESIS_TIME": 1704085200,
104+
"GENESIS_VALIDATORS": [],
105+
}
106+
)
107+
config = GenesisConfig.from_json(json_content)
108+
validators = config.to_validators()
109+
110+
assert len(validators.data) == 0
111+
112+
113+
class TestGenesisConfigState:
114+
"""Tests for state creation."""
115+
116+
def test_create_state_returns_valid_genesis(self) -> None:
117+
"""State has correct genesis time and validators."""
118+
config = GenesisConfig.from_json(SAMPLE_JSON)
119+
state = config.create_state()
120+
121+
# Genesis time is stored in the state's config.
122+
assert state.config.genesis_time == config.genesis_time
123+
assert state.slot == Slot(0)
124+
assert len(state.validators.data) == 3
125+
126+
127+
class TestGenesisConfigValidation:
128+
"""Tests for validation errors."""
129+
130+
def test_invalid_pubkey_raises_validation_error(self) -> None:
131+
"""Rejects malformed hex."""
132+
json_content = json.dumps(
133+
{
134+
"GENESIS_TIME": 1704085200,
135+
"GENESIS_VALIDATORS": ["not_valid_hex"],
136+
}
137+
)
138+
with pytest.raises(ValidationError):
139+
GenesisConfig.from_json(json_content)
140+
141+
def test_wrong_length_pubkey_raises_error(self) -> None:
142+
"""Rejects pubkeys with wrong length."""
143+
json_content = json.dumps(
144+
{
145+
"GENESIS_TIME": 1704085200,
146+
"GENESIS_VALIDATORS": ["0x0011223344"],
147+
}
148+
)
149+
with pytest.raises(SSZValueError):
150+
GenesisConfig.from_json(json_content)
151+
152+
def test_missing_genesis_time_raises_error(self) -> None:
153+
"""Requires GENESIS_TIME field."""
154+
json_content = json.dumps(
155+
{
156+
"GENESIS_VALIDATORS": [SAMPLE_PUBKEY_1],
157+
}
158+
)
159+
with pytest.raises(ValidationError):
160+
GenesisConfig.from_json(json_content)
161+
162+
def test_missing_validators_raises_error(self) -> None:
163+
"""Requires GENESIS_VALIDATORS field."""
164+
json_content = json.dumps(
165+
{
166+
"GENESIS_TIME": 1704085200,
167+
}
168+
)
169+
with pytest.raises(ValidationError):
170+
GenesisConfig.from_json(json_content)
171+
172+
173+
class TestReamCompatibility:
174+
"""Tests for compatibility with ream config format."""
175+
176+
def test_ream_format_config(self) -> None:
177+
"""Loads config in ream format with 0x-prefixed pubkeys."""
178+
# This matches the format used in ream/bin/ream/assets/lean/config.yaml
179+
json_content = json.dumps(
180+
{
181+
"GENESIS_TIME": 1704085200,
182+
"GENESIS_VALIDATORS": [REAM_PUBKEY_1, REAM_PUBKEY_2, REAM_PUBKEY_3],
183+
}
184+
)
185+
config = GenesisConfig.from_json(json_content)
186+
187+
assert config.genesis_time == Uint64(1704085200)
188+
assert len(config.genesis_validators) == 3
189+
190+
# Verify first pubkey matches.
191+
expected_first = Bytes52(
192+
bytes.fromhex(
193+
"e2a03c16122c7e0f940e2301aa460c54a2e1e8343968bb2782f26636f051e65e"
194+
"c589c858b9c7980b276ebe550056b23f0bdc3b5a"
195+
)
196+
)
197+
assert config.genesis_validators[0] == expected_first

0 commit comments

Comments
 (0)