diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index 7b63b9a16..ad25affe3 100755 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -40,10 +40,7 @@ from typing import ClassVar, Literal from lean_spec.config import LEAN_ENV -from lean_spec.forks.lstar.containers import ( - AttestationData, - ValidatorIndices, -) +from lean_spec.forks.lstar.containers import AttestationData from lean_spec.forks.lstar.containers.block.types import ( AggregatedAttestations, AttestationSignatures, @@ -64,7 +61,7 @@ HashTreeOpening, Randomness, ) -from lean_spec.types import Bytes32, Slot, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, Slot, Uint64, ValidatorIndex, ValidatorIndices SecretField = Literal["attestation_secret", "proposal_secret"] """Discriminator for which secret key to load from a validator key pair.""" diff --git a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py index 1b08b8203..ec1e77825 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -8,10 +8,9 @@ from lean_spec.forks.lstar.containers.block.block import Block, BlockBody from lean_spec.forks.lstar.containers.block.types import AggregatedAttestations from lean_spec.forks.lstar.containers.state import State -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof -from lean_spec.types import Bytes32 +from lean_spec.types import Bytes32, ValidatorIndices from ..keys import XmssKeyManager from ..test_types import AggregatedAttestationSpec, BlockSpec, StateExpectation diff --git a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py index 2ed0e5574..c60f1aab7 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -7,7 +7,6 @@ from pydantic import Field from lean_spec.forks.lstar.containers.attestation import AggregatedAttestation -from lean_spec.forks.lstar.containers.attestation.aggregation_bits import AggregationBits from lean_spec.forks.lstar.containers.block import ( SignedBlock, ) @@ -16,7 +15,7 @@ AttestationSignatures, ) from lean_spec.forks.lstar.containers.state import State -from lean_spec.types import Boolean, ValidatorIndex +from lean_spec.types import AggregationBits, Boolean, ValidatorIndex from ..keys import XmssKeyManager from ..test_types import BlockSpec diff --git a/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py index 03910afcc..bd6d885e8 100644 --- a/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py +++ b/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py @@ -6,9 +6,16 @@ from lean_spec.forks.lstar.containers.block.block import Block from lean_spec.forks.lstar.containers.block.types import AggregatedAttestations from lean_spec.forks.lstar.containers.state import State -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof -from lean_spec.types import ByteListMiB, Bytes32, CamelModel, Checkpoint, Slot, ValidatorIndex +from lean_spec.types import ( + ByteListMiB, + Bytes32, + CamelModel, + Checkpoint, + Slot, + ValidatorIndex, + ValidatorIndices, +) from ..keys import XmssKeyManager from .utils import resolve_checkpoint diff --git a/packages/testing/src/consensus_testing/test_types/block_spec.py b/packages/testing/src/consensus_testing/test_types/block_spec.py index e7e37ffb6..ed9d8c6b0 100644 --- a/packages/testing/src/consensus_testing/test_types/block_spec.py +++ b/packages/testing/src/consensus_testing/test_types/block_spec.py @@ -21,13 +21,12 @@ AttestationSignatures, ) from lean_spec.forks.lstar.containers.state import State -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.forks.lstar.store import Store from lean_spec.subspecs.chain.clock import Interval from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.types import Bytes32, CamelModel, Slot, ValidatorIndex +from lean_spec.types import Bytes32, CamelModel, Slot, ValidatorIndex, ValidatorIndices from ..keys import LEAN_ENV_TO_SCHEMES, XmssKeyManager, create_dummy_signature from .aggregated_attestation_spec import AggregatedAttestationSpec diff --git a/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py index b964e3100..ce452aa6b 100644 --- a/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py +++ b/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py @@ -6,9 +6,16 @@ from lean_spec.forks.lstar.containers.attestation.attestation import SignedAggregatedAttestation from lean_spec.forks.lstar.containers.block.block import Block from lean_spec.forks.lstar.containers.state import State -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof -from lean_spec.types import ByteListMiB, Bytes32, CamelModel, Checkpoint, Slot, ValidatorIndex +from lean_spec.types import ( + ByteListMiB, + Bytes32, + CamelModel, + Checkpoint, + Slot, + ValidatorIndex, + ValidatorIndices, +) from ..keys import XmssKeyManager from .utils import resolve_checkpoint diff --git a/src/lean_spec/forks/__init__.py b/src/lean_spec/forks/__init__.py index c7a4cc044..8f99bc7b5 100644 --- a/src/lean_spec/forks/__init__.py +++ b/src/lean_spec/forks/__init__.py @@ -1,6 +1,21 @@ """Multi-fork dispatch layer for leanSpec consensus specification.""" -from .lstar.containers.state import State +from .lstar.containers import ( + AggregatedAttestation, + Attestation, + AttestationData, + Block, + BlockBody, + BlockHeader, + Config, + SignedAggregatedAttestation, + SignedAttestation, + SignedBlock, + Validator, +) +from .lstar.containers.block import BlockLookup, BlockSignatures +from .lstar.containers.block.types import AggregatedAttestations, AttestationSignatures +from .lstar.containers.state import State, Validators from .lstar.spec import LstarSpec from .lstar.store import AttestationSignatureEntry, Store from .protocol import ForkProtocol, SpecStateType, SpecStoreType @@ -13,14 +28,30 @@ """Shared registry over the registered forks. Convenient for top-level callers.""" __all__ = [ + "AggregatedAttestation", + "AggregatedAttestations", + "Attestation", + "AttestationData", "AttestationSignatureEntry", + "AttestationSignatures", + "Block", + "BlockBody", + "BlockHeader", + "BlockLookup", + "BlockSignatures", + "Config", "DEFAULT_REGISTRY", "FORK_SEQUENCE", "ForkProtocol", "ForkRegistry", "LstarSpec", + "SignedAggregatedAttestation", + "SignedAttestation", + "SignedBlock", "SpecStateType", "SpecStoreType", "State", "Store", + "Validator", + "Validators", ] diff --git a/src/lean_spec/forks/lstar/containers/__init__.py b/src/lean_spec/forks/lstar/containers/__init__.py index 3f66e752d..6fb87283e 100644 --- a/src/lean_spec/forks/lstar/containers/__init__.py +++ b/src/lean_spec/forks/lstar/containers/__init__.py @@ -10,7 +10,6 @@ from .attestation import ( AggregatedAttestation, - AggregationBits, Attestation, AttestationData, SignedAggregatedAttestation, @@ -23,11 +22,10 @@ SignedBlock, ) from .config import Config -from .validator import Validator, ValidatorIndices +from .validator import Validator __all__ = [ "AggregatedAttestation", - "AggregationBits", "Attestation", "AttestationData", "Block", @@ -38,5 +36,4 @@ "SignedAttestation", "SignedBlock", "Validator", - "ValidatorIndices", ] diff --git a/src/lean_spec/forks/lstar/containers/attestation/__init__.py b/src/lean_spec/forks/lstar/containers/attestation/__init__.py index 8a2c45376..96cb69476 100644 --- a/src/lean_spec/forks/lstar/containers/attestation/__init__.py +++ b/src/lean_spec/forks/lstar/containers/attestation/__init__.py @@ -1,6 +1,5 @@ """Attestation containers and related types for the Lean spec.""" -from .aggregation_bits import AggregationBits from .attestation import ( AggregatedAttestation, Attestation, @@ -11,7 +10,6 @@ __all__ = [ "AggregatedAttestation", - "AggregationBits", "Attestation", "AttestationData", "SignedAggregatedAttestation", diff --git a/src/lean_spec/forks/lstar/containers/attestation/aggregation_bits.py b/src/lean_spec/forks/lstar/containers/attestation/aggregation_bits.py deleted file mode 100644 index 8ed3c5f23..000000000 --- a/src/lean_spec/forks/lstar/containers/attestation/aggregation_bits.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Aggregation bits for tracking validator participation.""" - -from __future__ import annotations - -from lean_spec.forks.lstar.containers.validator import ValidatorIndices -from lean_spec.subspecs.chain.config import VALIDATOR_REGISTRY_LIMIT -from lean_spec.types import ValidatorIndex -from lean_spec.types.bitfields import BaseBitlist - - -class AggregationBits(BaseBitlist): - """ - Bitlist representing validator participation in an attestation or signature. - - A general-purpose bitfield for tracking which validators have participated - in some collective action (attestation, signature aggregation, etc.). - """ - - LIMIT = int(VALIDATOR_REGISTRY_LIMIT) - - def to_validator_indices(self) -> ValidatorIndices: - """ - Extract all validator indices encoded in these aggregation bits. - - Returns: - ValidatorIndices containing the indices, sorted in ascending order. - - Raises: - AssertionError: If no bits are set. - """ - # Extract indices where bit is set; fail if none found. - indices = [ValidatorIndex(i) for i, bit in enumerate(self.data) if bool(bit)] - if not indices: - raise AssertionError("Aggregated attestation must reference at least one validator") - - return ValidatorIndices(data=indices) diff --git a/src/lean_spec/forks/lstar/containers/attestation/attestation.py b/src/lean_spec/forks/lstar/containers/attestation/attestation.py index d92d4243a..959fde36f 100644 --- a/src/lean_spec/forks/lstar/containers/attestation/attestation.py +++ b/src/lean_spec/forks/lstar/containers/attestation/attestation.py @@ -15,9 +15,7 @@ from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.types import Checkpoint, Container, Slot, ValidatorIndex - -from .aggregation_bits import AggregationBits +from lean_spec.types import AggregationBits, Checkpoint, Container, Slot, ValidatorIndex class AttestationData(Container): diff --git a/src/lean_spec/forks/lstar/containers/validator.py b/src/lean_spec/forks/lstar/containers/validator.py index cfc96c509..b4392659e 100644 --- a/src/lean_spec/forks/lstar/containers/validator.py +++ b/src/lean_spec/forks/lstar/containers/validator.py @@ -2,49 +2,9 @@ from __future__ import annotations -import lean_spec.forks.lstar.containers.attestation.aggregation_bits as _aggregation_bits from lean_spec.subspecs.chain.config import VALIDATOR_REGISTRY_LIMIT from lean_spec.subspecs.xmss.containers import PublicKey -from lean_spec.types import Boolean, Bytes52, Container, SSZList, ValidatorIndex - - -class ValidatorIndices(SSZList[ValidatorIndex]): - """List of validator indices up to registry limit.""" - - LIMIT = int(VALIDATOR_REGISTRY_LIMIT) - - def to_aggregation_bits(self) -> _aggregation_bits.AggregationBits: - """ - Convert to aggregation bits marking which validators are present. - - Returns: - AggregationBits with the corresponding indices set to True. - - Raises: - AssertionError: If no indices are provided. - AssertionError: If any index is outside the supported LIMIT. - """ - index_list = self.data - - # Require at least one validator for a valid aggregation. - if not index_list: - raise AssertionError("Aggregated attestation must reference at least one validator") - - # Convert to a set of native ints. - # - # This combines int conversion and deduplication in a single O(N) pass. - ids = {int(i) for i in index_list} - - # Validate bounds: max index must be within registry limit. - if (max_id := max(ids)) >= _aggregation_bits.AggregationBits.LIMIT: - raise AssertionError("Validator index out of range for aggregation bits") - - # Build bit list: - # - True at positions present in indices, - # - False elsewhere. - return _aggregation_bits.AggregationBits( - data=[Boolean(i in ids) for i in range(max_id + 1)] - ) +from lean_spec.types import Bytes52, Container, SSZList, ValidatorIndex class Validator(Container): diff --git a/src/lean_spec/forks/lstar/spec.py b/src/lean_spec/forks/lstar/spec.py index 9177cc3c1..f66b230a9 100644 --- a/src/lean_spec/forks/lstar/spec.py +++ b/src/lean_spec/forks/lstar/spec.py @@ -2,10 +2,22 @@ from typing import ClassVar -from lean_spec.forks.lstar.containers.block import Block +from lean_spec.forks.lstar.containers import ( + AggregatedAttestation, + Attestation, + AttestationData, + Block, + Config, + SignedAggregatedAttestation, + SignedAttestation, + SignedBlock, + Validator, +) +from lean_spec.forks.lstar.containers.block.block import BlockSignatures +from lean_spec.forks.lstar.containers.state import State +from lean_spec.forks.lstar.containers.validator import Validators from ..protocol import ForkProtocol, SpecStateType -from .containers.state import State from .store import Store @@ -20,8 +32,23 @@ class LstarSpec(ForkProtocol): state_class: type[State] = State block_class: type[Block] = Block + signed_block_class: type[SignedBlock] = SignedBlock + block_signatures_class: type[BlockSignatures] = BlockSignatures store_class: type[Store] = Store + attestation_data_class: type[AttestationData] = AttestationData + attestation_class: type[Attestation] = Attestation + signed_attestation_class: type[SignedAttestation] = SignedAttestation + aggregated_attestation_class: type[AggregatedAttestation] = AggregatedAttestation + signed_aggregated_attestation_class: type[SignedAggregatedAttestation] = ( + SignedAggregatedAttestation + ) + + validator_class: type[Validator] = Validator + validators_class: type[Validators] = Validators + + config_class: type[Config] = Config + def upgrade_state(self, state: SpecStateType) -> State: """ Lstar is the root fork: there is no predecessor, so no migration. diff --git a/src/lean_spec/forks/lstar/store.py b/src/lean_spec/forks/lstar/store.py index 2d46da384..fab5b2a58 100644 --- a/src/lean_spec/forks/lstar/store.py +++ b/src/lean_spec/forks/lstar/store.py @@ -18,7 +18,6 @@ ) from lean_spec.forks.lstar.containers.attestation.attestation import SignedAggregatedAttestation from lean_spec.forks.lstar.containers.block import BlockLookup -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.chain.clock import Interval from lean_spec.subspecs.chain.config import ( GOSSIP_DISPARITY_INTERVALS, @@ -42,6 +41,7 @@ Uint8, Uint64, ValidatorIndex, + ValidatorIndices, ) from lean_spec.types.base import StrictBaseModel diff --git a/src/lean_spec/forks/protocol.py b/src/lean_spec/forks/protocol.py index 1bf6cac21..75e5b3400 100644 --- a/src/lean_spec/forks/protocol.py +++ b/src/lean_spec/forks/protocol.py @@ -5,28 +5,221 @@ """ from abc import ABC, abstractmethod +from collections.abc import Mapping from typing import Any, ClassVar, Protocol, Self -from lean_spec.types import Bytes32, SSZList, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, Checkpoint, Slot, SSZList, Uint64, ValidatorIndex -class SpecStateType(Protocol): +class SpecSSZType(Protocol): + """Structural contract: any SSZ container exposes encode/decode.""" + + def encode_bytes(self) -> bytes: + """Serialize this container to its SSZ byte representation.""" + ... + + @classmethod + def decode_bytes(cls, data: bytes) -> Self: + """Deserialize an SSZ byte string into a new container instance.""" + ... + + +class SpecConfigType(SpecSSZType, Protocol): + """Structural contract: any fork's genesis Config container class.""" + + +class SpecStateType(SpecSSZType, Protocol): """Structural contract: any fork's State container class exposes genesis.""" + @property + def slot(self) -> Slot: + """Current slot of this state.""" + ... + + @property + def config(self) -> "SpecConfigType": + """Genesis configuration carried by the state.""" + ... + @classmethod def generate_genesis(cls, genesis_time: Uint64, validators: SSZList[Any]) -> Self: """Construct the fork's genesis state.""" ... -class SpecBlockType(Protocol): +class SpecBlockType(SpecSSZType, Protocol): """Structural contract: any fork's Block container class.""" + @property + def slot(self) -> Slot: + """Slot at which the block was proposed.""" + ... + + @property + def proposer_index(self) -> ValidatorIndex: + """Validator index of the block's proposer.""" + ... + + @property + def parent_root(self) -> Bytes32: + """SSZ root of the parent block.""" + ... + + @property + def state_root(self) -> Bytes32: + """SSZ root of the post-state produced by applying this block.""" + ... + + +class SpecSignedBlockType(SpecSSZType, Protocol): + """Structural contract: any fork's SignedBlock container class. + + A SignedBlock wraps a Block with its proposer + attestation signatures. + Subspecs treat instances as opaque SSZ-encodable payloads passed + between sync, gossip, and storage. + """ + + @property + def block(self) -> SpecBlockType: + """The wrapped Block payload.""" + ... + + +class SpecBlockSignaturesType(SpecSSZType, Protocol): + """Structural contract: any fork's BlockSignatures container class. + + Carries the proposer and attestation signature bundle for a block. + """ + + +class SpecAttestationDataType(SpecSSZType, Protocol): + """Structural contract: any fork's AttestationData container class. + + Encodes a validator's view of the chain (slot + source/target/head + checkpoints) and is the payload that gets signed. + """ + + @property + def slot(self) -> Slot: + """Slot the attestation is voting at.""" + ... + + @property + def head(self) -> Checkpoint: + """Head checkpoint the attestation votes for.""" + ... + + @property + def source(self) -> Checkpoint: + """Source checkpoint of the attestation.""" + ... + + @property + def target(self) -> Checkpoint: + """Target checkpoint of the attestation.""" + ... + + +class SpecAttestationType(SpecSSZType, Protocol): + """Structural contract: any fork's single-validator Attestation container class.""" + + @property + def data(self) -> SpecAttestationDataType: + """The unsigned attestation payload.""" + ... + + +class SpecSignedAttestationType(SpecSSZType, Protocol): + """Structural contract: any fork's SignedAttestation container class. + + A single validator's attestation bundled with its signature. + """ + + @property + def data(self) -> SpecAttestationDataType: + """The unsigned attestation payload.""" + ... + + @property + def validator_id(self) -> ValidatorIndex: + """Index of the validator that produced this attestation.""" + ... + + +class SpecAggregatedAttestationType(SpecSSZType, Protocol): + """Structural contract: any fork's AggregatedAttestation container class. + + An attestation aggregated over multiple validators via a participation + bitfield. + """ + + @property + def data(self) -> SpecAttestationDataType: + """The unsigned attestation payload.""" + ... + + +class SpecSignedAggregatedAttestationType(SpecSSZType, Protocol): + """Structural contract: any fork's SignedAggregatedAttestation container class. + + The aggregator's broadcast payload — combined attestation data plus the + aggregated signature proof. + """ + + @property + def data(self) -> SpecAttestationDataType: + """The unsigned attestation payload.""" + ... + + +class SpecValidatorType(SpecSSZType, Protocol): + """Structural contract: any fork's Validator container class. + + A single validator's static metadata (pubkeys, index). + """ + class SpecStoreType(Protocol): - """Structural contract: any fork's Store class exposes anchor construction.""" + """Structural contract: any fork's forkchoice Store. - head: Bytes32 + Exposes anchor construction plus the read/write surface that sync, + chain, and node services drive without depending on a concrete fork. + """ + + @property + def head(self) -> Bytes32: + """Root of the canonical head block.""" + ... + + @property + def safe_target(self) -> Bytes32: + """Root of the current safe target block.""" + ... + + @property + def latest_justified(self) -> Checkpoint: + """Most recent justified checkpoint.""" + ... + + @property + def latest_finalized(self) -> Checkpoint: + """Most recent finalized checkpoint.""" + ... + + @property + def validator_id(self) -> ValidatorIndex | None: + """Index of the local validator owning this store, if any.""" + ... + + @property + def blocks(self) -> Mapping[Bytes32, SpecBlockType]: + """Mapping from block root to known Block.""" + ... + + @property + def states(self) -> Mapping[Bytes32, SpecStateType]: + """Mapping from block root to post-state of that block.""" + ... @classmethod def from_anchor( @@ -38,6 +231,25 @@ def from_anchor( """Construct a forkchoice store anchored at the given state/block.""" ... + def on_block(self, signed_block: "SpecSignedBlockType") -> Self: + """Apply a signed block to the store and return the updated store.""" + ... + + def on_gossip_attestation( + self, + signed_attestation: "SpecSignedAttestationType", + is_aggregator: bool, + ) -> Self: + """Apply a single-validator attestation and return the updated store.""" + ... + + def on_gossip_aggregated_attestation( + self, + signed_attestation: "SpecSignedAggregatedAttestationType", + ) -> Self: + """Apply an aggregated attestation and return the updated store.""" + ... + class ForkProtocol(ABC): """Identity and construction facade for a devnet fork.""" @@ -70,9 +282,39 @@ class ForkProtocol(ABC): block_class: type[SpecBlockType] """Concrete Block container class owned by this fork.""" + signed_block_class: type[SpecSignedBlockType] + """Concrete SignedBlock container class — block + signatures envelope.""" + + block_signatures_class: type[SpecBlockSignaturesType] + """Concrete BlockSignatures container class — proposer + attestation signatures.""" + store_class: type[SpecStoreType] """Concrete forkchoice Store class owned by this fork.""" + attestation_data_class: type[SpecAttestationDataType] + """Concrete AttestationData container class.""" + + attestation_class: type[SpecAttestationType] + """Concrete Attestation container class — single-validator attestation.""" + + signed_attestation_class: type[SpecSignedAttestationType] + """Concrete SignedAttestation container class.""" + + aggregated_attestation_class: type[SpecAggregatedAttestationType] + """Concrete AggregatedAttestation container class.""" + + signed_aggregated_attestation_class: type[SpecSignedAggregatedAttestationType] + """Concrete SignedAggregatedAttestation container class.""" + + validator_class: type[SpecValidatorType] + """Concrete Validator container class — single validator's static metadata.""" + + validators_class: type[SSZList[Any]] + """Concrete Validators SSZList class — registry tracked in state.""" + + config_class: type[SpecConfigType] + """Concrete genesis Config container class.""" + def generate_genesis(self, genesis_time: Uint64, validators: SSZList[Any]) -> SpecStateType: """Construct a genesis state using this fork's State class.""" return self.state_class.generate_genesis(genesis_time, validators) diff --git a/src/lean_spec/subspecs/chain/config.py b/src/lean_spec/subspecs/chain/config.py index dff4e4f92..f22123efa 100644 --- a/src/lean_spec/subspecs/chain/config.py +++ b/src/lean_spec/subspecs/chain/config.py @@ -2,7 +2,20 @@ from typing import Final -from lean_spec.types import Uint8, Uint64 +from lean_spec.types import VALIDATOR_REGISTRY_LIMIT, Uint8, Uint64 + +__all__ = [ + "ATTESTATION_COMMITTEE_COUNT", + "GOSSIP_DISPARITY_INTERVALS", + "HISTORICAL_ROOTS_LIMIT", + "INTERVALS_PER_SLOT", + "JUSTIFICATION_LOOKBACK_SLOTS", + "MAX_ATTESTATIONS_DATA", + "MILLISECONDS_PER_INTERVAL", + "MILLISECONDS_PER_SLOT", + "SECONDS_PER_SLOT", + "VALIDATOR_REGISTRY_LIMIT", +] INTERVALS_PER_SLOT: Final = Uint64(5) """Number of intervals per slot for forkchoice processing.""" @@ -37,9 +50,6 @@ of approximately 12.1 days. """ -VALIDATOR_REGISTRY_LIMIT: Final = Uint64(2**12) -"""The maximum number of validators that can be in the registry.""" - ATTESTATION_COMMITTEE_COUNT: Final = Uint64(1) """The number of attestation committees per slot.""" diff --git a/src/lean_spec/subspecs/chain/service.py b/src/lean_spec/subspecs/chain/service.py index b4ed687dd..a03d21a92 100644 --- a/src/lean_spec/subspecs/chain/service.py +++ b/src/lean_spec/subspecs/chain/service.py @@ -26,9 +26,7 @@ import logging from dataclasses import dataclass, field -from lean_spec.forks.lstar.containers.attestation.attestation import ( - SignedAggregatedAttestation, -) +from lean_spec.forks import SignedAggregatedAttestation from lean_spec.subspecs.chain.config import INTERVALS_PER_SLOT from lean_spec.subspecs.sync import SyncService from lean_spec.types import Uint64 diff --git a/src/lean_spec/subspecs/genesis/config.py b/src/lean_spec/subspecs/genesis/config.py index e35b42675..8def42516 100644 --- a/src/lean_spec/subspecs/genesis/config.py +++ b/src/lean_spec/subspecs/genesis/config.py @@ -20,8 +20,7 @@ import yaml from pydantic import Field, field_validator, model_validator -from lean_spec.forks.lstar.containers import Validator -from lean_spec.forks.lstar.containers.state import Validators +from lean_spec.forks import Validator, Validators from lean_spec.types import Bytes52, StrictBaseModel, Uint64, ValidatorIndex diff --git a/src/lean_spec/subspecs/networking/client/event_source/gossip.py b/src/lean_spec/subspecs/networking/client/event_source/gossip.py index 7254f84f8..6360891dd 100644 --- a/src/lean_spec/subspecs/networking/client/event_source/gossip.py +++ b/src/lean_spec/subspecs/networking/client/event_source/gossip.py @@ -38,11 +38,7 @@ from dataclasses import dataclass -from lean_spec.forks.lstar.containers import SignedBlock -from lean_spec.forks.lstar.containers.attestation import ( - SignedAggregatedAttestation, - SignedAttestation, -) +from lean_spec.forks import SignedAggregatedAttestation, SignedAttestation, SignedBlock from lean_spec.snappy import SnappyDecompressionError, decompress from lean_spec.subspecs.networking.gossipsub.topic import ( ForkMismatchError, diff --git a/src/lean_spec/subspecs/networking/client/event_source/live.py b/src/lean_spec/subspecs/networking/client/event_source/live.py index 90bf0fb4d..7ba11b57c 100644 --- a/src/lean_spec/subspecs/networking/client/event_source/live.py +++ b/src/lean_spec/subspecs/networking/client/event_source/live.py @@ -59,11 +59,7 @@ import logging from dataclasses import dataclass, field -from lean_spec.forks.lstar.containers import SignedBlock -from lean_spec.forks.lstar.containers.attestation import ( - SignedAggregatedAttestation, - SignedAttestation, -) +from lean_spec.forks import SignedAggregatedAttestation, SignedAttestation, SignedBlock from lean_spec.subspecs.networking.config import ( GOSSIPSUB_DEFAULT_PROTOCOL_ID, GOSSIPSUB_PROTOCOL_ID_V12, diff --git a/src/lean_spec/subspecs/networking/client/reqresp_client.py b/src/lean_spec/subspecs/networking/client/reqresp_client.py index 37798fde8..adb8d09ff 100644 --- a/src/lean_spec/subspecs/networking/client/reqresp_client.py +++ b/src/lean_spec/subspecs/networking/client/reqresp_client.py @@ -32,7 +32,7 @@ import logging from dataclasses import dataclass, field -from lean_spec.forks.lstar.containers import SignedBlock +from lean_spec.forks import SignedBlock from lean_spec.subspecs.networking.config import MAX_REQUEST_BLOCKS from lean_spec.subspecs.networking.reqresp.codec import ( CodecError, diff --git a/src/lean_spec/subspecs/networking/reqresp/handler.py b/src/lean_spec/subspecs/networking/reqresp/handler.py index e7c886dbf..e443d1350 100644 --- a/src/lean_spec/subspecs/networking/reqresp/handler.py +++ b/src/lean_spec/subspecs/networking/reqresp/handler.py @@ -64,7 +64,7 @@ from dataclasses import dataclass from typing import Final -from lean_spec.forks.lstar.containers import SignedBlock +from lean_spec.forks import SignedBlock from lean_spec.snappy import SnappyDecompressionError, frame_decompress from lean_spec.subspecs.networking.config import ( MAX_ERROR_MESSAGE_SIZE, diff --git a/src/lean_spec/subspecs/networking/service/events.py b/src/lean_spec/subspecs/networking/service/events.py index a9b52ebad..9c2453925 100644 --- a/src/lean_spec/subspecs/networking/service/events.py +++ b/src/lean_spec/subspecs/networking/service/events.py @@ -23,11 +23,7 @@ from dataclasses import dataclass -from lean_spec.forks.lstar.containers import SignedBlock -from lean_spec.forks.lstar.containers.attestation import ( - SignedAggregatedAttestation, - SignedAttestation, -) +from lean_spec.forks import SignedAggregatedAttestation, SignedAttestation, SignedBlock from lean_spec.subspecs.networking.gossipsub.topic import GossipTopic from lean_spec.subspecs.networking.reqresp.message import Status from lean_spec.subspecs.networking.transport import PeerId diff --git a/src/lean_spec/subspecs/networking/service/service.py b/src/lean_spec/subspecs/networking/service/service.py index 13f379a59..c2e848408 100644 --- a/src/lean_spec/subspecs/networking/service/service.py +++ b/src/lean_spec/subspecs/networking/service/service.py @@ -25,11 +25,7 @@ import logging from dataclasses import dataclass, field -from lean_spec.forks.lstar.containers import SignedBlock -from lean_spec.forks.lstar.containers.attestation import ( - SignedAggregatedAttestation, - SignedAttestation, -) +from lean_spec.forks import SignedAggregatedAttestation, SignedAttestation, SignedBlock from lean_spec.snappy import compress from lean_spec.subspecs.networking.client.event_source import EventSource from lean_spec.subspecs.networking.gossipsub.topic import GossipTopic diff --git a/src/lean_spec/subspecs/node/node.py b/src/lean_spec/subspecs/node/node.py index ff184f13c..b9a0bd26c 100644 --- a/src/lean_spec/subspecs/node/node.py +++ b/src/lean_spec/subspecs/node/node.py @@ -19,15 +19,17 @@ from pathlib import Path from typing import Final, cast -from lean_spec.forks import ForkProtocol, State, Store -from lean_spec.forks.lstar.containers import ( +from lean_spec.forks import ( + AggregatedAttestations, Block, BlockBody, + ForkProtocol, + SignedAttestation, SignedBlock, + State, + Store, + Validators, ) -from lean_spec.forks.lstar.containers.attestation import SignedAttestation -from lean_spec.forks.lstar.containers.block.types import AggregatedAttestations -from lean_spec.forks.lstar.containers.state import Validators from lean_spec.subspecs.api import AggregatorController, ApiServer, ApiServerConfig from lean_spec.subspecs.chain import SlotClock from lean_spec.subspecs.chain.clock import Interval @@ -197,7 +199,12 @@ def from_genesis(cls, config: NodeConfig) -> Node: # The database is optional - nodes can run without persistence. database: Database | None = None if config.database_path is not None: - database = SQLiteDatabase(config.database_path, config.fork.state_class) + database = SQLiteDatabase( + config.database_path, + state_class=config.fork.state_class, + block_class=config.fork.block_class, + attestation_data_class=config.fork.attestation_data_class, + ) # # If database contains valid state, resume from there. @@ -416,12 +423,7 @@ def _try_load_store_from_database( # # The store starts with just the head block and state. # Additional blocks can be loaded on demand or via sync. - if fork is not None: - store_cls = fork.store_class - else: - from lean_spec.forks.lstar.store import Store - - store_cls = Store + store_cls = fork.store_class if fork is not None else Store return store_cls( time=Interval(store_time), config=head_state.config, diff --git a/src/lean_spec/subspecs/storage/database.py b/src/lean_spec/subspecs/storage/database.py index 3df7d892b..2746296b1 100644 --- a/src/lean_spec/subspecs/storage/database.py +++ b/src/lean_spec/subspecs/storage/database.py @@ -11,9 +11,11 @@ from contextlib import contextmanager from typing import Protocol -from lean_spec.forks import State -from lean_spec.forks.lstar.containers import Block -from lean_spec.forks.lstar.containers.attestation import AttestationData +from lean_spec.forks.protocol import ( + SpecAttestationDataType, + SpecBlockType, + SpecStateType, +) from lean_spec.types import Bytes32, Checkpoint, Slot, Uint64, ValidatorIndex @@ -35,7 +37,7 @@ class Database(Protocol): # Block Operations - def get_block(self, root: Bytes32) -> Block | None: + def get_block(self, root: Bytes32) -> SpecBlockType | None: """ Retrieve a block by its root hash. @@ -47,7 +49,7 @@ def get_block(self, root: Bytes32) -> Block | None: """ ... - def put_block(self, block: Block, root: Bytes32) -> None: + def put_block(self, block: SpecBlockType, root: Bytes32) -> None: """ Store a block with its root hash. @@ -71,7 +73,7 @@ def has_block(self, root: Bytes32) -> bool: # State Operations - def get_state(self, root: Bytes32) -> State | None: + def get_state(self, root: Bytes32) -> SpecStateType | None: """ Retrieve a state by its associated block root. @@ -83,7 +85,7 @@ def get_state(self, root: Bytes32) -> State | None: """ ... - def put_state(self, state: State, root: Bytes32) -> None: + def put_state(self, state: SpecStateType, root: Bytes32) -> None: """ Store a state indexed by its associated block root. @@ -145,7 +147,9 @@ def put_finalized_checkpoint(self, checkpoint: Checkpoint) -> None: # Attestation Operations - def get_latest_attestation(self, validator_index: ValidatorIndex) -> AttestationData | None: + def get_latest_attestation( + self, validator_index: ValidatorIndex + ) -> SpecAttestationDataType | None: """ Retrieve the latest attestation for a validator. @@ -160,7 +164,7 @@ def get_latest_attestation(self, validator_index: ValidatorIndex) -> Attestation def put_latest_attestation( self, validator_index: ValidatorIndex, - attestation: AttestationData, + attestation: SpecAttestationDataType, ) -> None: """ Store the latest attestation for a validator. @@ -171,7 +175,7 @@ def put_latest_attestation( """ ... - def get_all_latest_attestations(self) -> dict[ValidatorIndex, AttestationData]: + def get_all_latest_attestations(self) -> dict[ValidatorIndex, SpecAttestationDataType]: """ Retrieve all latest attestations. diff --git a/src/lean_spec/subspecs/storage/sqlite.py b/src/lean_spec/subspecs/storage/sqlite.py index 6078d0e7c..1d4224f63 100644 --- a/src/lean_spec/subspecs/storage/sqlite.py +++ b/src/lean_spec/subspecs/storage/sqlite.py @@ -23,9 +23,11 @@ from contextlib import contextmanager from pathlib import Path -from lean_spec.forks import State -from lean_spec.forks.lstar.containers import Block -from lean_spec.forks.lstar.containers.attestation import AttestationData +from lean_spec.forks.protocol import ( + SpecAttestationDataType, + SpecBlockType, + SpecStateType, +) from lean_spec.types import Bytes32, Checkpoint, Slot, Uint64, ValidatorIndex from .exceptions import StorageCorruptionError, StorageReadError, StorageWriteError @@ -52,7 +54,13 @@ class SQLiteDatabase: Writes are buffered until explicitly committed via commit() or batch_write(). """ - def __init__(self, path: Path | str, state_class: type[State] = State) -> None: + def __init__( + self, + path: Path | str, + state_class: type[SpecStateType], + block_class: type[SpecBlockType], + attestation_data_class: type[SpecAttestationDataType], + ) -> None: """ Initialize SQLite database. @@ -62,9 +70,13 @@ def __init__(self, path: Path | str, state_class: type[State] = State) -> None: path: Path to SQLite database file. Use ":memory:" for in-memory database. state_class: State class used to decode SSZ bytes. + block_class: Block class used to decode SSZ bytes. + attestation_data_class: AttestationData class used to decode SSZ bytes. """ self._path = Path(path) if isinstance(path, str) else path self._state_class = state_class + self._block_class = block_class + self._attestation_data_class = attestation_data_class # SQLite handles concurrent access through file-level locking. # @@ -120,7 +132,7 @@ def _init_schema(self) -> None: # Block Operations - def get_block(self, root: Bytes32) -> Block | None: + def get_block(self, root: Bytes32) -> SpecBlockType | None: """Retrieve a block by its root hash.""" try: cursor = self._conn.cursor() @@ -141,11 +153,11 @@ def get_block(self, root: Bytes32) -> Block | None: return None try: - return Block.decode_bytes(row["data"]) + return self._block_class.decode_bytes(row["data"]) except Exception as e: raise StorageCorruptionError(f"Corrupt block data for root {root.hex()}: {e}") from e - def put_block(self, block: Block, root: Bytes32) -> None: + def put_block(self, block: SpecBlockType, root: Bytes32) -> None: """Store a block with its root hash.""" try: cursor = self._conn.cursor() @@ -189,7 +201,7 @@ def has_block(self, root: Bytes32) -> bool: # They are large (~2MB+) and expensive to compute from scratch. # Storing states enables fast re-initialization and historical queries. - def get_state(self, root: Bytes32) -> State | None: + def get_state(self, root: Bytes32) -> SpecStateType | None: """Retrieve a state by its associated block root.""" try: cursor = self._conn.cursor() @@ -211,7 +223,7 @@ def get_state(self, root: Bytes32) -> State | None: f"Corrupt state data for block root {root.hex()}: {e}" ) from e - def put_state(self, state: State, root: Bytes32) -> None: + def put_state(self, state: SpecStateType, root: Bytes32) -> None: """Store a state indexed by its associated block root.""" try: cursor = self._conn.cursor() @@ -336,7 +348,9 @@ def put_finalized_checkpoint(self, checkpoint: Checkpoint) -> None: # Fork choice uses the latest attestation from each validator # to determine which branch has the most support. - def get_latest_attestation(self, validator_index: ValidatorIndex) -> AttestationData | None: + def get_latest_attestation( + self, validator_index: ValidatorIndex + ) -> SpecAttestationDataType | None: """Retrieve the latest attestation for a validator.""" try: cursor = self._conn.cursor() @@ -359,7 +373,7 @@ def get_latest_attestation(self, validator_index: ValidatorIndex) -> Attestation return None try: - return AttestationData.decode_bytes(row["data"]) + return self._attestation_data_class.decode_bytes(row["data"]) except Exception as e: raise StorageCorruptionError( f"Corrupt attestation data for validator {validator_index}: {e}" @@ -368,7 +382,7 @@ def get_latest_attestation(self, validator_index: ValidatorIndex) -> Attestation def put_latest_attestation( self, validator_index: ValidatorIndex, - attestation: AttestationData, + attestation: SpecAttestationDataType, ) -> None: """Store the latest attestation for a validator.""" try: @@ -390,7 +404,7 @@ def put_latest_attestation( f"Failed to write attestation for validator {validator_index}: {e}" ) from e - def get_all_latest_attestations(self) -> dict[ValidatorIndex, AttestationData]: + def get_all_latest_attestations(self) -> dict[ValidatorIndex, SpecAttestationDataType]: """Retrieve all latest attestations.""" try: cursor = self._conn.cursor() @@ -406,7 +420,9 @@ def get_all_latest_attestations(self) -> dict[ValidatorIndex, AttestationData]: try: return { - ValidatorIndex(row["validator_index"]): AttestationData.decode_bytes(row["data"]) + ValidatorIndex(row["validator_index"]): self._attestation_data_class.decode_bytes( + row["data"] + ) for row in rows } except Exception as e: diff --git a/src/lean_spec/subspecs/sync/backfill_sync.py b/src/lean_spec/subspecs/sync/backfill_sync.py index c6c994aba..ed7c53d7b 100644 --- a/src/lean_spec/subspecs/sync/backfill_sync.py +++ b/src/lean_spec/subspecs/sync/backfill_sync.py @@ -42,7 +42,7 @@ from dataclasses import dataclass, field from typing import Protocol -from lean_spec.forks.lstar.containers import SignedBlock +from lean_spec.forks import SignedBlock from lean_spec.subspecs.networking.config import MAX_REQUEST_BLOCKS from lean_spec.subspecs.networking.transport.peer_id import PeerId from lean_spec.types import Bytes32, Slot, Uint64 diff --git a/src/lean_spec/subspecs/sync/block_cache.py b/src/lean_spec/subspecs/sync/block_cache.py index cbad36238..817dba185 100644 --- a/src/lean_spec/subspecs/sync/block_cache.py +++ b/src/lean_spec/subspecs/sync/block_cache.py @@ -43,8 +43,7 @@ from dataclasses import dataclass, field from time import time -from lean_spec.forks import Store -from lean_spec.forks.lstar.containers import SignedBlock +from lean_spec.forks import SignedBlock, Store from lean_spec.subspecs.networking.transport.peer_id import PeerId from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Slot diff --git a/src/lean_spec/subspecs/sync/head_sync.py b/src/lean_spec/subspecs/sync/head_sync.py index ea6fa158a..8637b0c04 100644 --- a/src/lean_spec/subspecs/sync/head_sync.py +++ b/src/lean_spec/subspecs/sync/head_sync.py @@ -48,8 +48,7 @@ from collections.abc import Callable from dataclasses import dataclass, field -from lean_spec.forks import Store -from lean_spec.forks.lstar.containers import SignedBlock +from lean_spec.forks import SignedBlock, Store from lean_spec.subspecs.networking.transport.peer_id import PeerId from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Slot, Uint64 diff --git a/src/lean_spec/subspecs/sync/service.py b/src/lean_spec/subspecs/sync/service.py index b46bb242d..07f2894fd 100644 --- a/src/lean_spec/subspecs/sync/service.py +++ b/src/lean_spec/subspecs/sync/service.py @@ -40,14 +40,14 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass, field -from lean_spec.forks import Store -from lean_spec.forks.lstar.containers import ( +from lean_spec.forks import ( Block, + BlockLookup, SignedAggregatedAttestation, SignedAttestation, SignedBlock, + Store, ) -from lean_spec.forks.lstar.containers.block import BlockLookup from lean_spec.subspecs.chain.clock import SlotClock from lean_spec.subspecs.metrics import registry as metrics from lean_spec.subspecs.networking.reqresp.message import Status diff --git a/src/lean_spec/subspecs/validator/registry.py b/src/lean_spec/subspecs/validator/registry.py index 9de146ac5..93861d67d 100644 --- a/src/lean_spec/subspecs/validator/registry.py +++ b/src/lean_spec/subspecs/validator/registry.py @@ -32,9 +32,8 @@ import yaml from pydantic import BaseModel, field_validator -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.xmss import SecretKey -from lean_spec.types import Bytes52, ValidatorIndex +from lean_spec.types import Bytes52, ValidatorIndex, ValidatorIndices logger = logging.getLogger(__name__) diff --git a/src/lean_spec/subspecs/validator/service.py b/src/lean_spec/subspecs/validator/service.py index 93854e95d..2857dc35a 100644 --- a/src/lean_spec/subspecs/validator/service.py +++ b/src/lean_spec/subspecs/validator/service.py @@ -37,16 +37,14 @@ from dataclasses import dataclass, field from typing import Literal -from lean_spec.forks.lstar.containers import ( +from lean_spec.forks import ( AttestationData, + AttestationSignatures, Block, + BlockSignatures, SignedAttestation, SignedBlock, ) -from lean_spec.forks.lstar.containers.block import ( - AttestationSignatures, - BlockSignatures, -) from lean_spec.subspecs.chain.clock import Interval, SlotClock from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.sync import SyncService diff --git a/src/lean_spec/subspecs/xmss/aggregation.py b/src/lean_spec/subspecs/xmss/aggregation.py index 77d64bcd8..28863ca55 100644 --- a/src/lean_spec/subspecs/xmss/aggregation.py +++ b/src/lean_spec/subspecs/xmss/aggregation.py @@ -13,9 +13,15 @@ ) from lean_spec.config import LEAN_ENV, LeanEnvMode -from lean_spec.forks.lstar.containers.attestation import AggregationBits -from lean_spec.forks.lstar.containers.validator import ValidatorIndices -from lean_spec.types import ByteListMiB, Bytes32, Container, Slot, ValidatorIndex +from lean_spec.types import ( + AggregationBits, + ByteListMiB, + Bytes32, + Container, + Slot, + ValidatorIndex, + ValidatorIndices, +) from .containers import PublicKey, Signature diff --git a/src/lean_spec/types/__init__.py b/src/lean_spec/types/__init__.py index 1b8283675..85acf38e9 100644 --- a/src/lean_spec/types/__init__.py +++ b/src/lean_spec/types/__init__.py @@ -1,5 +1,6 @@ """Reusable type definitions for the Lean Ethereum specification.""" +from .aggregation import VALIDATOR_REGISTRY_LIMIT, AggregationBits, ValidatorIndices from .base import CamelModel, StrictBaseModel from .bitfields import BaseBitlist, BaseBitvector from .boolean import Boolean @@ -66,11 +67,14 @@ "Boolean", "Container", # Domain types — fork-stable + "AggregationBits", "Checkpoint", "IMMEDIATE_JUSTIFICATION_WINDOW", "Slot", "SubnetId", + "VALIDATOR_REGISTRY_LIMIT", "ValidatorIndex", + "ValidatorIndices", # RLP encoding/decoding "encode_rlp", "decode_rlp", diff --git a/src/lean_spec/types/aggregation.py b/src/lean_spec/types/aggregation.py new file mode 100644 index 000000000..333c8af4e --- /dev/null +++ b/src/lean_spec/types/aggregation.py @@ -0,0 +1,87 @@ +""" +Validator participation: bitlist + index-list pair. + +Two SSZ types co-located because they round-trip: + +- `AggregationBits.to_validator_indices()` returns the indices set in the bitlist. +- `ValidatorIndices.to_aggregation_bits()` packs an index list back into bits. + +Both are sized by `VALIDATOR_REGISTRY_LIMIT`. The shape is inherited by every +validator-keyed bitfield and index list across the spec. +""" + +from typing import Final + +from lean_spec.types.bitfields import BaseBitlist +from lean_spec.types.boolean import Boolean +from lean_spec.types.collections import SSZList +from lean_spec.types.uint import Uint64 +from lean_spec.types.validator import ValidatorIndex + +VALIDATOR_REGISTRY_LIMIT: Final = Uint64(2**12) +"""The maximum number of validators that can be in the registry.""" + + +class AggregationBits(BaseBitlist): + """ + Bitlist representing validator participation in an attestation or signature. + + A general-purpose bitfield for tracking which validators have participated + in some collective action (attestation, signature aggregation, etc.). + """ + + LIMIT = int(VALIDATOR_REGISTRY_LIMIT) + + def to_validator_indices(self) -> "ValidatorIndices": + """ + Extract all validator indices encoded in these aggregation bits. + + Returns: + ValidatorIndices containing the indices, sorted in ascending order. + + Raises: + AssertionError: If no bits are set. + """ + # Extract indices where bit is set; fail if none found. + indices = [ValidatorIndex(i) for i, bit in enumerate(self.data) if bool(bit)] + if not indices: + raise AssertionError("Aggregated attestation must reference at least one validator") + + return ValidatorIndices(data=indices) + + +class ValidatorIndices(SSZList[ValidatorIndex]): + """List of validator indices up to the registry limit.""" + + LIMIT = int(VALIDATOR_REGISTRY_LIMIT) + + def to_aggregation_bits(self) -> AggregationBits: + """ + Convert to aggregation bits marking which validators are present. + + Returns: + AggregationBits with the corresponding indices set to True. + + Raises: + AssertionError: If no indices are provided. + AssertionError: If any index is outside the supported LIMIT. + """ + index_list = self.data + + # Require at least one validator for a valid aggregation. + if not index_list: + raise AssertionError("Aggregated attestation must reference at least one validator") + + # Convert to a set of native ints. + # + # This combines int conversion and deduplication in a single O(N) pass. + ids = {int(i) for i in index_list} + + # Validate bounds: max index must be within registry limit. + if (max_id := max(ids)) >= AggregationBits.LIMIT: + raise AssertionError("Validator index out of range for aggregation bits") + + # Build bit list: + # - True at positions present in indices, + # - False elsewhere. + return AggregationBits(data=[Boolean(i in ids) for i in range(max_id + 1)]) diff --git a/tests/consensus/devnet/ssz/test_consensus_containers.py b/tests/consensus/devnet/ssz/test_consensus_containers.py index df46c130c..333931ddf 100644 --- a/tests/consensus/devnet/ssz/test_consensus_containers.py +++ b/tests/consensus/devnet/ssz/test_consensus_containers.py @@ -18,7 +18,6 @@ SignedBlock, Validator, ) -from lean_spec.forks.lstar.containers.attestation import AggregationBits from lean_spec.forks.lstar.containers.block import BlockSignatures from lean_spec.forks.lstar.containers.block.types import ( AggregatedAttestations, @@ -33,6 +32,7 @@ from lean_spec.forks.lstar.containers.validator import Validators from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof from lean_spec.types import ( + AggregationBits, Boolean, ByteListMiB, Bytes32, diff --git a/tests/consensus/devnet/ssz/test_xmss_containers.py b/tests/consensus/devnet/ssz/test_xmss_containers.py index 3a54e7abf..c7cd583b6 100644 --- a/tests/consensus/devnet/ssz/test_xmss_containers.py +++ b/tests/consensus/devnet/ssz/test_xmss_containers.py @@ -4,7 +4,6 @@ from consensus_testing import SSZTestFiller from consensus_testing.keys import XmssKeyManager, create_dummy_signature -from lean_spec.forks.lstar.containers.attestation import AggregationBits from lean_spec.subspecs.koalabear import Fp from lean_spec.subspecs.xmss import PublicKey from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof @@ -16,7 +15,15 @@ HashTreeOpening, Parameter, ) -from lean_spec.types import Boolean, ByteListMiB, Bytes32, Slot, Uint64, ValidatorIndex +from lean_spec.types import ( + AggregationBits, + Boolean, + ByteListMiB, + Bytes32, + Slot, + Uint64, + ValidatorIndex, +) pytestmark = pytest.mark.valid_until("Lstar") diff --git a/tests/lean_spec/forks/test_fork_protocol.py b/tests/lean_spec/forks/test_fork_protocol.py index 8d5038935..011c5cb4b 100644 --- a/tests/lean_spec/forks/test_fork_protocol.py +++ b/tests/lean_spec/forks/test_fork_protocol.py @@ -1,10 +1,12 @@ """Tests for the multi-fork architecture.""" import ast +from pathlib import Path from typing import ClassVar import pytest +import lean_spec from lean_spec.forks import ( DEFAULT_REGISTRY, FORK_SEQUENCE, @@ -18,6 +20,18 @@ from lean_spec.types import Slot, Uint64 from tests.lean_spec.helpers.builders import make_validators +_LEAN_SPEC_FILE = lean_spec.__file__ +assert _LEAN_SPEC_FILE is not None # noqa: S101 +_SUBSPECS_ROOT: Path = Path(_LEAN_SPEC_FILE).parent / "subspecs" +"""Filesystem root for subspec source files. Used by import-guard tests.""" + +_FORBIDDEN_FORK_PREFIXES: tuple[str, ...] = ("lean_spec.forks.lstar",) +""" +Module prefixes that subspec code must never import directly. + +Subspecs are meant to be fork-agnostic shared libraries. +""" + class _NextSpec(LstarSpec): """Synthetic successor fork. Used only by registry tests.""" @@ -39,7 +53,7 @@ def test_protocol_module_imports_no_devnet_package(self) -> None: """The protocol module must not import any devnet package.""" source = protocol.__file__ assert source is not None - tree = ast.parse(open(source).read()) + tree = ast.parse(Path(source).read_text()) for node in ast.walk(tree): if isinstance(node, ast.ImportFrom): @@ -49,6 +63,26 @@ def test_protocol_module_imports_no_devnet_package(self) -> None: for alias in node.names: assert "devnet" not in alias.name, f"Forbidden import of {alias.name}" + def test_subspecs_do_not_import_concrete_fork(self) -> None: + """Subspecs must remain fork-agnostic.""" + offenders: list[str] = [] + for source_file in _SUBSPECS_ROOT.rglob("*.py"): + tree = ast.parse(source_file.read_text(), filename=str(source_file)) + location = source_file.relative_to(_SUBSPECS_ROOT.parent) + for node in ast.walk(tree): + # `from X import Y` — `X` is the module being imported from. + if isinstance(node, ast.ImportFrom): + mod = node.module or "" + if any(mod.startswith(p) for p in _FORBIDDEN_FORK_PREFIXES): + offenders.append(f"{location}:{node.lineno}: from {mod} import ...") + # `import X` — each `alias.name` is a fully-qualified module path. + elif isinstance(node, ast.Import): + for alias in node.names: + if any(alias.name.startswith(p) for p in _FORBIDDEN_FORK_PREFIXES): + offenders.append(f"{location}:{node.lineno}: import {alias.name}") + + assert not offenders, "Subspecs must not import concrete forks:\n" + "\n".join(offenders) + class TestLstarSpec: """Tests for the LstarSpec fork implementation.""" diff --git a/tests/lean_spec/helpers/builders.py b/tests/lean_spec/helpers/builders.py index 2443f0f8d..f7302e74e 100644 --- a/tests/lean_spec/helpers/builders.py +++ b/tests/lean_spec/helpers/builders.py @@ -30,7 +30,6 @@ AttestationSignatures, ) from lean_spec.forks.lstar.containers.state import Validators -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.chain.clock import Interval, SlotClock from lean_spec.subspecs.koalabear import Fp from lean_spec.subspecs.networking import PeerId @@ -51,7 +50,15 @@ HashTreeOpening, Randomness, ) -from lean_spec.types import Bytes32, Bytes52, Checkpoint, Slot, Uint64, ValidatorIndex +from lean_spec.types import ( + Bytes32, + Bytes52, + Checkpoint, + Slot, + Uint64, + ValidatorIndex, + ValidatorIndices, +) from .mocks import MockForkchoiceStore, MockNetworkRequester diff --git a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py index 387701a56..9dde98b34 100644 --- a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py +++ b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py @@ -4,11 +4,17 @@ from lean_spec.forks.lstar.containers.attestation import ( AggregatedAttestation, - AggregationBits, AttestationData, ) -from lean_spec.forks.lstar.containers.validator import ValidatorIndices -from lean_spec.types import Boolean, Bytes32, Checkpoint, Slot, ValidatorIndex +from lean_spec.types import ( + AggregationBits, + Boolean, + Bytes32, + Checkpoint, + Slot, + ValidatorIndex, + ValidatorIndices, +) class TestAggregationBits: diff --git a/tests/lean_spec/subspecs/containers/test_state_aggregation.py b/tests/lean_spec/subspecs/containers/test_state_aggregation.py index fc5c347c4..690443697 100644 --- a/tests/lean_spec/subspecs/containers/test_state_aggregation.py +++ b/tests/lean_spec/subspecs/containers/test_state_aggregation.py @@ -8,9 +8,8 @@ from lean_spec.forks.lstar.containers.attestation import AttestationData from lean_spec.forks.lstar.containers.block import Block, BlockBody from lean_spec.forks.lstar.containers.block.types import AggregatedAttestations -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Checkpoint, Slot, ValidatorIndex +from lean_spec.types import Bytes32, Checkpoint, Slot, ValidatorIndex, ValidatorIndices from tests.lean_spec.helpers import ( make_aggregated_proof, make_attestation_data_simple, diff --git a/tests/lean_spec/subspecs/containers/test_state_process_attestations.py b/tests/lean_spec/subspecs/containers/test_state_process_attestations.py index 65ca07581..b64a9860e 100644 --- a/tests/lean_spec/subspecs/containers/test_state_process_attestations.py +++ b/tests/lean_spec/subspecs/containers/test_state_process_attestations.py @@ -47,8 +47,7 @@ HistoricalBlockHashes, JustifiedSlots, ) -from lean_spec.forks.lstar.containers.validator import ValidatorIndices -from lean_spec.types import Boolean, Checkpoint, Slot, ValidatorIndex +from lean_spec.types import Boolean, Checkpoint, Slot, ValidatorIndex, ValidatorIndices from tests.lean_spec.helpers import make_bytes32, make_genesis_state diff --git a/tests/lean_spec/subspecs/forkchoice/test_compute_block_weights.py b/tests/lean_spec/subspecs/forkchoice/test_compute_block_weights.py index de056ee2b..ec659ea00 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_compute_block_weights.py +++ b/tests/lean_spec/subspecs/forkchoice/test_compute_block_weights.py @@ -4,10 +4,9 @@ from lean_spec.forks.lstar import Store from lean_spec.forks.lstar.containers.attestation import AttestationData -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof -from lean_spec.types import Checkpoint, Slot, ValidatorIndex +from lean_spec.types import Checkpoint, Slot, ValidatorIndex, ValidatorIndices from lean_spec.types.byte_arrays import ByteListMiB from tests.lean_spec.helpers import make_bytes32, make_signed_block diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py index 02097e866..4f7cb5a9c 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py +++ b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py @@ -11,12 +11,19 @@ SignedAggregatedAttestation, SignedAttestation, ) -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.chain.clock import Interval from lean_spec.subspecs.chain.config import INTERVALS_PER_SLOT from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof -from lean_spec.types import ByteListMiB, Bytes32, Checkpoint, Slot, Uint64, ValidatorIndex +from lean_spec.types import ( + ByteListMiB, + Bytes32, + Checkpoint, + Slot, + Uint64, + ValidatorIndex, + ValidatorIndices, +) from tests.lean_spec.helpers import ( TEST_VALIDATOR_ID, make_aggregated_proof, diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_pruning.py b/tests/lean_spec/subspecs/forkchoice/test_store_pruning.py index 8944a4fe4..abf314867 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_store_pruning.py +++ b/tests/lean_spec/subspecs/forkchoice/test_store_pruning.py @@ -1,9 +1,8 @@ """Tests for Store attestation data pruning.""" from lean_spec.forks.lstar import AttestationSignatureEntry, Store -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof -from lean_spec.types import ByteListMiB, Bytes32, Slot, ValidatorIndex +from lean_spec.types import ByteListMiB, Bytes32, Slot, ValidatorIndex, ValidatorIndices from tests.lean_spec.helpers import ( make_attestation_data, make_bytes32, diff --git a/tests/lean_spec/subspecs/node/test_node.py b/tests/lean_spec/subspecs/node/test_node.py index da3f43fef..14c754c73 100644 --- a/tests/lean_spec/subspecs/node/test_node.py +++ b/tests/lean_spec/subspecs/node/test_node.py @@ -12,6 +12,7 @@ from lean_spec.forks.lstar import State from lean_spec.forks.lstar.containers import ( + AttestationData, Block, BlockBody, ) @@ -80,7 +81,7 @@ def _make_populated_db( justifications_validators=JustificationValidators(data=[]), ) - db = SQLiteDatabase(":memory:", State) + db = SQLiteDatabase(":memory:", State, Block, AttestationData) db.put_block(block, head_root) db.put_state(state, head_root) db.put_head_root(head_root) diff --git a/tests/lean_spec/subspecs/storage/test_sqlite.py b/tests/lean_spec/subspecs/storage/test_sqlite.py index 5325109ef..24bf0b10b 100644 --- a/tests/lean_spec/subspecs/storage/test_sqlite.py +++ b/tests/lean_spec/subspecs/storage/test_sqlite.py @@ -28,7 +28,7 @@ @pytest.fixture def db() -> Generator[SQLiteDatabase, None, None]: """Create an in-memory SQLite database for testing.""" - database = SQLiteDatabase(":memory:", State) + database = SQLiteDatabase(":memory:", State, Block, AttestationData) yield database database.close() @@ -354,7 +354,7 @@ def test_persist_and_reload_genesis(self, genesis_block: Block, genesis_state: S genesis_time = Uint64(1000) # Write genesis data and close. - with SQLiteDatabase(db_path, State) as db: + with SQLiteDatabase(db_path, State, Block, AttestationData) as db: with db.batch_write(): db.put_block(genesis_block, block_root) db.put_state(genesis_state, block_root) @@ -365,7 +365,7 @@ def test_persist_and_reload_genesis(self, genesis_block: Block, genesis_state: S db.put_genesis_time(genesis_time) # Reopen and verify all data survived. - with SQLiteDatabase(db_path, State) as db: + with SQLiteDatabase(db_path, State, Block, AttestationData) as db: assert db.get_head_root() == block_root assert db.get_block(block_root) == genesis_block assert db.get_state(block_root) == genesis_state @@ -586,7 +586,7 @@ class TestLifecycle: def test_context_manager(self) -> None: """Database works as context manager.""" - with SQLiteDatabase(":memory:", State) as db: + with SQLiteDatabase(":memory:", State, Block, AttestationData) as db: root = Bytes32(b"\x0c" * 32) db.put_head_root(root) db.commit() diff --git a/tests/lean_spec/subspecs/validator/test_service.py b/tests/lean_spec/subspecs/validator/test_service.py index 87ceed32d..e89c139bf 100644 --- a/tests/lean_spec/subspecs/validator/test_service.py +++ b/tests/lean_spec/subspecs/validator/test_service.py @@ -13,7 +13,6 @@ Block, SignedAttestation, SignedBlock, - ValidatorIndices, ) from lean_spec.subspecs.chain.clock import SlotClock from lean_spec.subspecs.chain.config import MILLISECONDS_PER_INTERVAL @@ -25,7 +24,7 @@ from lean_spec.subspecs.validator.registry import ValidatorEntry from lean_spec.subspecs.xmss import TARGET_SIGNATURE_SCHEME from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof -from lean_spec.types import Bytes32, Slot, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, Slot, Uint64, ValidatorIndex, ValidatorIndices from tests.lean_spec.helpers import ( TEST_VALIDATOR_ID, MockNetworkRequester, diff --git a/tests/lean_spec/subspecs/xmss/test_aggregation.py b/tests/lean_spec/subspecs/xmss/test_aggregation.py index ac416e63a..806a3e296 100644 --- a/tests/lean_spec/subspecs/xmss/test_aggregation.py +++ b/tests/lean_spec/subspecs/xmss/test_aggregation.py @@ -5,10 +5,9 @@ import pytest from consensus_testing.keys import XmssKeyManager -from lean_spec.forks.lstar.containers.validator import ValidatorIndices from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof, AggregationError -from lean_spec.types import ByteListMiB, Checkpoint, Slot, ValidatorIndex +from lean_spec.types import ByteListMiB, Checkpoint, Slot, ValidatorIndex, ValidatorIndices from tests.lean_spec.helpers import make_attestation_data_simple, make_bytes32