diff --git a/src/lean_spec/node/genesis.py b/src/lean_spec/node/genesis.py new file mode 100644 index 000000000..536c8cf05 --- /dev/null +++ b/src/lean_spec/node/genesis.py @@ -0,0 +1,82 @@ +"""Genesis configuration loader for the cross-client YAML format.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml +from pydantic import Field, field_validator + +from lean_spec.base import StrictBaseModel +from lean_spec.spec.forks import ( + VALIDATOR_REGISTRY_LIMIT, + Validator, + ValidatorIndex, + Validators, +) +from lean_spec.spec.ssz import Bytes52, Uint64 + + +class GenesisValidatorEntry(StrictBaseModel): + """A single validator's public keys in the genesis configuration.""" + + attestation_public_key: Bytes52 + """XMSS public key for signing attestations.""" + + proposal_public_key: Bytes52 + """XMSS public key the proposer signs the block root with.""" + + @field_validator("attestation_public_key", "proposal_public_key", mode="before") + @classmethod + def _yaml_int_to_hex(cls, field_value: Any) -> Any: + """ + Re-encode integer inputs as hex. + + A YAML parser reads an unquoted 0x value as an int rather than a hex string. + """ + if isinstance(field_value, int): + return f"0x{field_value:0{Bytes52.LENGTH * 2}x}" + return field_value + + +class GenesisConfig(StrictBaseModel): + """The network-wide origin: when slot 0 begins and which validators secure the chain.""" + + model_config = StrictBaseModel.model_config | {"extra": "ignore"} + + genesis_time: Uint64 = Field(alias="GENESIS_TIME") + """Unix timestamp in seconds when slot 0 begins.""" + + genesis_validators: list[GenesisValidatorEntry] = Field(alias="GENESIS_VALIDATORS") + """Validators present at slot 0, in registry order.""" + + @field_validator("genesis_validators", mode="before") + @classmethod + def _reject_oversized_validator_set(cls, genesis_validators: Any) -> Any: + """Bound the set before decoding.""" + registry_limit = int(VALIDATOR_REGISTRY_LIMIT) + if isinstance(genesis_validators, list) and len(genesis_validators) > registry_limit: + raise ValueError( + f"genesis validator count {len(genesis_validators)} " + f"exceeds registry limit {registry_limit}" + ) + return genesis_validators + + def to_validators(self) -> Validators: + """Build the validator set, assigning each one a sequential index from 0.""" + return Validators( + data=[ + Validator( + attestation_public_key=genesis_validator.attestation_public_key, + proposal_public_key=genesis_validator.proposal_public_key, + index=ValidatorIndex(validator_index), + ) + for validator_index, genesis_validator in enumerate(self.genesis_validators) + ] + ) + + @classmethod + def from_yaml_file(cls, path: Path | str) -> GenesisConfig: + """Load and validate configuration from a genesis YAML file.""" + return cls.model_validate(yaml.safe_load(Path(path).read_text(encoding="utf-8"))) diff --git a/src/lean_spec/node/genesis/__init__.py b/src/lean_spec/node/genesis/__init__.py deleted file mode 100644 index 9c381c401..000000000 --- a/src/lean_spec/node/genesis/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Genesis configuration and state initialization.""" - -from lean_spec.node.genesis.config import GenesisConfig - -__all__ = ["GenesisConfig"] diff --git a/src/lean_spec/node/genesis/config.py b/src/lean_spec/node/genesis/config.py deleted file mode 100644 index b46f6c78f..000000000 --- a/src/lean_spec/node/genesis/config.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Genesis configuration loader. - -Loads genesis configuration from YAML files compatible with ream and zeam. - -The expected YAML format matches the cross-client convention: - - GENESIS_TIME: 1704085200 - GENESIS_VALIDATORS: - - attestation_public_key: 0xe2a03c16122c7e0f... - proposal_public_key: 0x0767e65924063f79... - - attestation_public_key: 0xabcdef0123456789... - proposal_public_key: 0x9876543210fedcba... -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any - -import yaml -from pydantic import Field, field_validator - -from lean_spec.base import StrictBaseModel -from lean_spec.spec.forks import ( - VALIDATOR_REGISTRY_LIMIT, - Validator, - ValidatorIndex, - Validators, -) -from lean_spec.spec.ssz import Bytes52, Uint64 - - -class GenesisValidatorEntry(StrictBaseModel): - """A single validator's public keys in the genesis configuration.""" - - attestation_public_key: Bytes52 - """XMSS public key for signing attestations.""" - - proposal_public_key: Bytes52 - """XMSS public key for signing proposer attestations in blocks.""" - - @field_validator("attestation_public_key", "proposal_public_key", mode="before") - @classmethod - def _yaml_int_to_hex(cls, v: Any) -> Any: - """ - Re-encode integer inputs as hex strings before standard validation. - - A YAML parser may interpret an unquoted 0x-prefixed value as an int. - Converting it back to a hex string lets the byte-array schema handle it. - """ - if isinstance(v, int): - return f"0x{v:0104x}" - return v - - -class GenesisConfig(StrictBaseModel): - """ - Configuration that establishes the birth of an Ethereum consensus chain. - - Genesis is the shared starting point for all participants in the network. - Without a common genesis, nodes cannot agree on the chain's history. - Every block traces its ancestry back to this origin. - - The genesis configuration solves two fundamental coordination problems: - - - Time Synchronization: All nodes agree on when slots begin - - Initial Trust: Bootstrap validators that can produce and attest blocks - - Field names use UPPERCASE to match cross-client YAML convention. - Pydantic aliases map them to snake_case Python attributes. - - Extra YAML keys (e.g. ACTIVE_EPOCH, VALIDATOR_COUNT) are ignored so configs - from lean-quickstart and other generators load without error. - """ - - model_config = StrictBaseModel.model_config | {"extra": "ignore"} - - genesis_time: Uint64 = Field(alias="GENESIS_TIME") - """ - Unix timestamp (seconds since 1970-01-01 UTC) when slot 0 begins. - - Anchors the chain's clock to real-world time. - - Nodes compute the current slot as: (now - genesis_time) / slot_duration. - - Immutable once the chain launches. - """ - - genesis_validators: list[GenesisValidatorEntry] = Field(alias="GENESIS_VALIDATORS") - """ - Validators trusted to secure the chain from slot 0. - - Each entry contains two XMSS public keys: - - - attestation_public_key: for signing attestations - - proposal_public_key: for signing proposer attestations in blocks - - Security note: 2/3+ collusion controls the chain until new validators join. - """ - - @field_validator("genesis_validators") - @classmethod - def _reject_oversized_validator_set( - cls, genesis_validators: list[GenesisValidatorEntry] - ) -> list[GenesisValidatorEntry]: - """ - Bound the genesis validator set against the registry limit. - - The state registry holds at most this many validators. - A genesis list larger than the limit can never fit on chain. - Each entry also triggers an XMSS public-key decode, so an unbounded - list lets a malicious config exhaust memory during load. - """ - if len(genesis_validators) > int(VALIDATOR_REGISTRY_LIMIT): - raise ValueError( - f"genesis validator count {len(genesis_validators)} " - f"exceeds registry limit {int(VALIDATOR_REGISTRY_LIMIT)}" - ) - return genesis_validators - - def to_validators(self) -> Validators: - """ - Build the genesis validator set with assigned indices. - - Each validator needs an index for the registry. - Indices are assigned sequentially starting from 0. - """ - return Validators( - data=[ - Validator( - attestation_public_key=genesis_validator.attestation_public_key, - proposal_public_key=genesis_validator.proposal_public_key, - index=ValidatorIndex(validator_index), - ) - for validator_index, genesis_validator in enumerate(self.genesis_validators) - ] - ) - - @classmethod - def from_yaml_file(cls, path: Path | str) -> GenesisConfig: - """ - Load configuration from a YAML file. - - Use this to load shared genesis files distributed to all clients. - Compatible with ream's config.yaml format. - - Raises: - FileNotFoundError: If the file does not exist. - yaml.YAMLError: If the file is not valid YAML. - pydantic.ValidationError: If the data fails validation. - """ - path = Path(path) - with path.open(encoding="utf-8") as yaml_file: - parsed_yaml = yaml.safe_load(yaml_file) - return cls.model_validate(parsed_yaml) diff --git a/tests/node/genesis/__init__.py b/tests/node/genesis/__init__.py deleted file mode 100644 index 828578123..000000000 --- a/tests/node/genesis/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the genesis configuration module.""" diff --git a/tests/node/genesis/test_config.py b/tests/node/test_genesis.py similarity index 100% rename from tests/node/genesis/test_config.py rename to tests/node/test_genesis.py