diff --git a/src/lean_spec/subspecs/chain/config.py b/src/lean_spec/subspecs/chain/config.py index 63397b73b..ef0052236 100644 --- a/src/lean_spec/subspecs/chain/config.py +++ b/src/lean_spec/subspecs/chain/config.py @@ -71,9 +71,8 @@ of approximately 12.1 days. """ -VALIDATOR_REGISTRY_LIMIT: Final = Uint64(2**12) -"""The maximum number of validators that can be in the registry.""" - +STAKER_REGISTRY_LIMIT: Final = Uint64(2**12) +"""The maximum number of stakers that can be in the registry.""" class _ChainConfig(BaseModel): """ @@ -95,7 +94,7 @@ class _ChainConfig(BaseModel): # State List Length Presets historical_roots_limit: Uint64 - validator_registry_limit: Uint64 + staker_registry_limit: Uint64 # The Devnet Chain Configuration. @@ -108,5 +107,5 @@ class _ChainConfig(BaseModel): fast_confirm_due_bps=FAST_CONFIRM_DUE_BPS, view_freeze_cutoff_bps=VIEW_FREEZE_CUTOFF_BPS, historical_roots_limit=HISTORICAL_ROOTS_LIMIT, - validator_registry_limit=VALIDATOR_REGISTRY_LIMIT, + staker_registry_limit=STAKER_REGISTRY_LIMIT, ) diff --git a/src/lean_spec/subspecs/containers/__init__.py b/src/lean_spec/subspecs/containers/__init__.py index 7fd55d5f0..5c253239b 100644 --- a/src/lean_spec/subspecs/containers/__init__.py +++ b/src/lean_spec/subspecs/containers/__init__.py @@ -3,6 +3,7 @@ from .block import Block, BlockBody, BlockHeader, SignedBlock from .checkpoint import Checkpoint from .config import Config +from .staker import Staker from .state import State from .vote import SignedVote, Vote @@ -14,6 +15,7 @@ "Config", "SignedBlock", "SignedVote", + "Staker", "State", "Vote", ] diff --git a/src/lean_spec/subspecs/containers/block.py b/src/lean_spec/subspecs/containers/block.py index fbcfe381d..8cb2ba9c6 100644 --- a/src/lean_spec/subspecs/containers/block.py +++ b/src/lean_spec/subspecs/containers/block.py @@ -11,7 +11,7 @@ class BlockBody(Container): """The body of a block, containing payload data.""" - attestations: List[SignedVote, config.VALIDATOR_REGISTRY_LIMIT.as_int()] # type: ignore + attestations: List[SignedVote, config.STAKER_REGISTRY_LIMIT.as_int()] # type: ignore """ A list of votes included in the block. diff --git a/src/lean_spec/subspecs/containers/staker.py b/src/lean_spec/subspecs/containers/staker.py new file mode 100644 index 000000000..90bf925fd --- /dev/null +++ b/src/lean_spec/subspecs/containers/staker.py @@ -0,0 +1,65 @@ +""" +Staker Container + +Lean Consensus participants are part of a unified pool of stakers, +as described in this [3TS design proposal]( +https://ethresear.ch/t/three-tier-staking-3ts-unbundling-attesters-includers +-and-execution-proposers/21648/1). + +Each slot, the **Staker** chooses from three different available roles: +- Attester +- Includer +- (Execution) Proposer + +Each staker explicitly opts into the role(s) that they wish to take as +protocol participants. +One, two, or all three roles can be chosen, based on the stakers preferences +and level of sophistication. +Mixing and matching multiple roles is possible, under certain constraints. + +Each role can be delegated to a target staker and each role can be set as +delegatable for other stakers to execute as operators. +The staker can update the staking configuration at any time. +""" + +from pydantic import Field +from typing_extensions import Annotated + +from lean_spec.subspecs.staker import AttesterRole, IncluderRole, ProposerRole, StakerSettings +from lean_spec.types import StrictBaseModel + + +class Staker(StrictBaseModel): + """The consensus staker object.""" + + role_config: Annotated[ + list[StakerSettings], + Field(min_length=3, max_length=3), + ] + """The list contains the settings for each of the roles the staker can + activate.""" + + attester_role: AttesterRole + """ + Contains the state related to the Attester role. + + This role is responsible for providing economic security by voting on the + validity of blocks. This field tracks all attestation-specific data. + """ + + includer_role: IncluderRole + """ + Contains the state related to the Includer role. + + This role upholds censorship resistance by creating inclusion lists (ILs) + that constrain block producers. This field tracks all inclusion-specific + data. + """ + + proposer_role: ProposerRole + """ + Contains the state related to the Execution Proposer role. + + This role focuses on performance by building and proposing valuable + execution blocks, including transaction ordering and MEV extraction. + """ diff --git a/src/lean_spec/subspecs/containers/state.py b/src/lean_spec/subspecs/containers/state.py index ef3373b34..527bd39cc 100644 --- a/src/lean_spec/subspecs/containers/state.py +++ b/src/lean_spec/subspecs/containers/state.py @@ -8,7 +8,14 @@ from lean_spec.subspecs.chain import config as chainconfig from lean_spec.subspecs.ssz.constants import ZERO_HASH from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Boolean, Bytes32, Container, Uint64, ValidatorIndex, is_proposer +from lean_spec.types import ( + Boolean, + Bytes32, + Container, + StakerIndex, + Uint64, + is_proposer, +) from lean_spec.types import List as SSZList from lean_spec.types.bitfields import Bitlist @@ -16,6 +23,7 @@ from .checkpoint import Checkpoint from .config import Config from .slot import Slot +from .staker import Staker from .vote import SignedVote, Vote @@ -57,6 +65,9 @@ class State(Container): ] """A bitlist of validators who participated in justifications.""" + stakers: SSZList[Staker, DEVNET_CONFIG.staker_registry_limit.as_int()] # type: ignore + """The list of stakers in the state.""" + @classmethod def generate_genesis(cls, genesis_time: Uint64, num_validators: Uint64) -> "State": """ @@ -79,7 +90,7 @@ def generate_genesis(cls, genesis_time: Uint64, num_validators: Uint64) -> "Stat # Create the zeroed header that anchors the chain at genesis. genesis_header = BlockHeader( slot=Slot(0), - proposer_index=ValidatorIndex(0), + proposer_index=StakerIndex(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), body_root=hash_tree_root(BlockBody(attestations=[])), @@ -92,6 +103,7 @@ def generate_genesis(cls, genesis_time: Uint64, num_validators: Uint64) -> "Stat num_validators=num_validators, ), slot=Slot(0), + stakers=[], latest_block_header=genesis_header, latest_justified=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), latest_finalized=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), @@ -101,14 +113,14 @@ def generate_genesis(cls, genesis_time: Uint64, num_validators: Uint64) -> "Stat justifications_validators=[], ) - def is_proposer(self, validator_index: ValidatorIndex) -> bool: + def is_proposer(self, staker_index: StakerIndex) -> bool: """ Check if a validator is the proposer for the current slot. The proposer selection follows a simple round-robin mechanism based on the slot number and the total number of validators. """ - return is_proposer(validator_index, Uint64(self.slot.as_int()), self.config.num_validators) + return is_proposer(staker_index, Uint64(self.slot.as_int()), self.config.num_validators) def get_justifications(self) -> Dict[Bytes32, List[Boolean]]: """ @@ -141,7 +153,7 @@ def get_justifications(self) -> Dict[Bytes32, List[Boolean]]: # Cache the validator registry limit for concise slicing calculations. # # This value determines the size of the block of votes for each root. - limit = DEVNET_CONFIG.validator_registry_limit.as_int() + limit = DEVNET_CONFIG.staker_registry_limit.as_int() roots = self.justifications_roots validators = self.justifications_validators @@ -172,7 +184,7 @@ def with_justifications(self, justifications: Dict[Bytes32, List[Boolean]]) -> " Raises: AssertionError: If any vote list's length does not match the - `validator_registry_limit`. + `staker_registry_limit`. """ # It will store the deterministically sorted list of roots. new_roots = SSZList[Bytes32, DEVNET_CONFIG.historical_roots_limit.as_int()]() # type: ignore @@ -182,7 +194,7 @@ def with_justifications(self, justifications: Dict[Bytes32, List[Boolean]]) -> " DEVNET_CONFIG.historical_roots_limit.as_int() * DEVNET_CONFIG.historical_roots_limit.as_int(), ]() # type: ignore - limit = DEVNET_CONFIG.validator_registry_limit.as_int() + limit = DEVNET_CONFIG.staker_registry_limit.as_int() # Iterate through the roots in sorted order for deterministic output. for root in sorted(justifications.keys()): @@ -438,7 +450,8 @@ def process_operations(self, body: BlockBody) -> "State": def process_attestations( self, - attestations: SSZList[SignedVote, chainconfig.VALIDATOR_REGISTRY_LIMIT.as_int()], # type: ignore + attestations: SSZList[SignedVote, + chainconfig.STAKER_REGISTRY_LIMIT.as_int()], # type: ignore ) -> "State": """ Apply attestation votes and update justification/finalization @@ -545,7 +558,7 @@ def process_attestations( # Track a unique vote for (target_root, validator_id) only if target_root not in justifications: # Initialize a fresh bitvector for this target root (all False). - limit = DEVNET_CONFIG.validator_registry_limit.as_int() + limit = DEVNET_CONFIG.staker_registry_limit.as_int() justifications[target_root] = [Boolean(False)] * limit validator_id = vote.validator_id.as_int() diff --git a/src/lean_spec/subspecs/forkchoice/helpers.py b/src/lean_spec/subspecs/forkchoice/helpers.py index 9ae3ef096..298b05064 100644 --- a/src/lean_spec/subspecs/forkchoice/helpers.py +++ b/src/lean_spec/subspecs/forkchoice/helpers.py @@ -7,7 +7,7 @@ from typing import Dict, Optional from lean_spec.subspecs.containers import Block, Checkpoint, State -from lean_spec.types import Bytes32, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex from .constants import ZERO_HASH @@ -15,7 +15,7 @@ def get_fork_choice_head( blocks: Dict[Bytes32, Block], root: Bytes32, - latest_votes: Dict[ValidatorIndex, Checkpoint], + latest_votes: Dict[StakerIndex, Checkpoint], min_score: int = 0, ) -> Bytes32: """ diff --git a/src/lean_spec/subspecs/forkchoice/store.py b/src/lean_spec/subspecs/forkchoice/store.py index 1e788bbd0..37e1fd1c8 100644 --- a/src/lean_spec/subspecs/forkchoice/store.py +++ b/src/lean_spec/subspecs/forkchoice/store.py @@ -23,7 +23,7 @@ ) from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex, is_proposer +from lean_spec.types import Bytes32, StakerIndex, Uint64, is_proposer from lean_spec.types.container import Container from .helpers import get_fork_choice_head, get_latest_justified @@ -61,10 +61,10 @@ class Store(Container): states: Dict[Bytes32, "State"] = {} """Mapping from state root to State objects.""" - latest_known_votes: Dict[ValidatorIndex, Checkpoint] = {} + latest_known_votes: Dict[StakerIndex, Checkpoint] = {} """Latest votes by validator that have been processed.""" - latest_new_votes: Dict[ValidatorIndex, Checkpoint] = {} + latest_new_votes: Dict[StakerIndex, Checkpoint] = {} """Latest votes by validator that are pending processing.""" @classmethod @@ -152,7 +152,7 @@ def process_attestation(self, signed_vote: "SignedVote", is_from_block: bool = F # Validate attestation structure and constraints self.validate_attestation(signed_vote) - validator_id = ValidatorIndex(signed_vote.data.validator_id) + validator_id = StakerIndex(signed_vote.data.validator_id) vote = signed_vote.data if is_from_block: @@ -365,7 +365,7 @@ def get_vote_target(self) -> Checkpoint: target_block = self.blocks[target_block_root] return Checkpoint(root=hash_tree_root(target_block), slot=target_block.slot) - def produce_block(self, slot: Slot, validator_index: ValidatorIndex) -> Block: + def produce_block(self, slot: Slot, validator_index: StakerIndex) -> Block: """ Produce a new block for the given slot and validator. @@ -470,7 +470,7 @@ def produce_block(self, slot: Slot, validator_index: ValidatorIndex) -> Block: return finalized_block - def produce_attestation_vote(self, slot: Slot, validator_index: ValidatorIndex) -> Vote: + def produce_attestation_vote(self, slot: Slot, validator_index: StakerIndex) -> Vote: """ Produce an attestation vote for the given slot and validator. diff --git a/src/lean_spec/subspecs/staker/__init__.py b/src/lean_spec/subspecs/staker/__init__.py new file mode 100644 index 000000000..892bc1637 --- /dev/null +++ b/src/lean_spec/subspecs/staker/__init__.py @@ -0,0 +1,10 @@ +""" +Specifications for the staker's protocol participation roles and +settings. +""" + +from .config import DEVNET_STAKER_CONFIG +from .role import AttesterRole, IncluderRole, ProposerRole +from .settings import StakerSettings + +__all__ = ["DEVNET_STAKER_CONFIG", "AttesterRole", "IncluderRole", "ProposerRole", "StakerSettings"] diff --git a/src/lean_spec/subspecs/staker/config.py b/src/lean_spec/subspecs/staker/config.py new file mode 100644 index 000000000..583e17a50 --- /dev/null +++ b/src/lean_spec/subspecs/staker/config.py @@ -0,0 +1,23 @@ +"""This file defines the parameters used for the Staker and staking.""" + +from typing_extensions import Final + +from lean_spec.types import StrictBaseModel, Uint64 + +DELEGATIONS_REGISTRY_LIMIT: Uint64 = Uint64(2**12) +"""The maximum number of delegations that can be stored in the state, per staker role.""" + + +class _StakerConfig(StrictBaseModel): + """ + A model holding the canonical, immutable configuration constants + for the Staker and staking. + """ + + delegations_registry_limit: Uint64 + + +DEVNET_STAKER_CONFIG: Final = _StakerConfig( + delegations_registry_limit=DELEGATIONS_REGISTRY_LIMIT, +) +"""The Devnet Staker Configuration.""" diff --git a/src/lean_spec/subspecs/staker/role.py b/src/lean_spec/subspecs/staker/role.py new file mode 100644 index 000000000..e824fec9a --- /dev/null +++ b/src/lean_spec/subspecs/staker/role.py @@ -0,0 +1,121 @@ +"""This defines the roles a Staker can activate for itself.""" + +from typing import List + +from lean_spec.subspecs.staker import DEVNET_STAKER_CONFIG +from lean_spec.types import Epoch, Gwei, StrictBaseModel, Uint64 + + +class AttesterRole(StrictBaseModel): + """Parameters for the Attester Role.""" + + activation_eligibility_epoch: Epoch + """The epoch in which the role enters the activation queue.""" + + activation_epoch: Epoch + """The epoch in which the role becomes active.""" + + exit_epoch: Epoch + """The epoch in which the delegated balance stops participating in the + protocol but remains accountable.""" + + withdrawable_epoch: Epoch + """The epoch in which the delegated balance is no longer held + accountable and is credited to the delegator.""" + + is_active: bool + """Determines if the role is active or not.""" + + balance: Gwei + """The actual balance of the staker's role.""" + + slashed: bool + """Determines if the staker has been slashed for this role.""" + + staker_quota: Uint64 + """The quota of the staker's role in the delegation.""" + + delegations_quotas: List[Uint64, + DEVNET_STAKER_CONFIG.delegations_registry_limit] + """The quotas of each delegated balance for this role. This list is + parallel with the stakers list from the state.""" + + delegated_balances: List[Uint64, + DEVNET_STAKER_CONFIG.delegations_registry_limit] + """The delegated balances for each staker. This list is parallel with + the stakers list from the state.""" + + total_delegated_balance: Gwei + """This is the sum of every value in `delegated_balances` and is used + for performance optimisation purposes.""" + + +class IncluderRole(StrictBaseModel): + """Parameters for the Includer Role.""" + + is_active: bool + """Determines if the role is active or not.""" + + balance: Gwei + """The actual balance of the staker's role.""" + + staker_quota: Uint64 + """The quota of the staker's role in the delegation.""" + + delegations_quotas: List[Uint64, + DEVNET_STAKER_CONFIG.delegations_registry_limit] + """The quotas of each delegated balance for this role. This list is + parallel with the stakers list from the state.""" + + delegated_balances: List[Uint64, + DEVNET_STAKER_CONFIG.delegations_registry_limit] + """The delegated balances for each staker. This list is parallel with + the stakers list from the state.""" + + total_delegated_balance: Gwei + """This is the sum of every value in `delegated_balances` and is used + for performance optimisation purposes.""" + + +class ProposerRole(StrictBaseModel): + """Parameters for the Proposer Role.""" + + activation_eligibility_epoch: Epoch + """The epoch in which the role enters the activation queue.""" + + activation_epoch: Epoch + """The epoch in which the role becomes active.""" + + exit_epoch: Epoch + """The epoch in which the delegated balance stops participating in the + protocol but remains accountable.""" + + withdrawable_epoch: Epoch + """The epoch in which the delegated balance is no longer held + accountable and is credited to the delegator.""" + + is_active: bool + """Determines if the role is active or not.""" + + balance: Gwei + """The actual balance of the staker's role.""" + + slashed: bool + """Determines if the staker has been slashed for this role.""" + + staker_quota: Uint64 + """The quota of the staker's role in the delegation.""" + + delegations_quotas: List[Uint64, + DEVNET_STAKER_CONFIG.delegations_registry_limit] + """The quotas of each delegated balance for this role. This list is + parallel with the stakers list from the state.""" + + delegated_balances: List[Uint64, + DEVNET_STAKER_CONFIG.delegations_registry_limit] + """The delegated balances for each staker. This list is parallel with + the stakers list from the state.""" + + total_delegated_balance: Gwei + """This is the sum of every value in `delegated_balances` and is used + for performance optimisation purposes.""" diff --git a/src/lean_spec/subspecs/staker/settings.py b/src/lean_spec/subspecs/staker/settings.py new file mode 100644 index 000000000..34c895726 --- /dev/null +++ b/src/lean_spec/subspecs/staker/settings.py @@ -0,0 +1,29 @@ +"""This defines the settings for a role a Staker can activate.""" + +from pydantic import BaseModel + +from lean_spec.types import Byte, StakerIndex, Uint64 + + +class StakerSettings(BaseModel): + """Parameters for the StakerSettings.""" + + role_identifier: Byte + """The role for which these settings are applied.""" + + active: bool + """Determines if the role should be active or not.""" + + delegated: bool + """Determines if the role should be delegated or not.""" + + target_staker: StakerIndex + """Represents the index of a target staker that should receive the + delegation.""" + + delegatable: bool + """Determines if the staker should accept delegations for this role.""" + + fee_quotient: Uint64 + """Defines the quotient used to calculate the fees that the delegate + owes to the staker.""" diff --git a/src/lean_spec/types/__init__.py b/src/lean_spec/types/__init__.py index c3067ba04..dded9c83e 100644 --- a/src/lean_spec/types/__init__.py +++ b/src/lean_spec/types/__init__.py @@ -3,18 +3,25 @@ from .base import StrictBaseModel from .basispt import BasisPoint from .boolean import Boolean +from .byte import Byte from .byte_arrays import Bytes32 from .collections import List, Vector from .container import Container +from .epoch import Epoch +from .gwei import Gwei +from .staker import StakerIndex from .uint import Uint64 -from .validator import ValidatorIndex, is_proposer +from .validator import is_proposer __all__ = [ "Uint64", "BasisPoint", "Bytes32", "StrictBaseModel", - "ValidatorIndex", + "Epoch", + "Gwei", + "StakerIndex", + "Byte", "is_proposer", "List", "Vector", diff --git a/src/lean_spec/types/epoch.py b/src/lean_spec/types/epoch.py new file mode 100644 index 000000000..e1946d5a7 --- /dev/null +++ b/src/lean_spec/types/epoch.py @@ -0,0 +1,6 @@ +"""Epoch Type Specification.""" + +from .uint import Uint64 + +Epoch = Uint64 +"""A type alias to represent an Epoch""" diff --git a/src/lean_spec/types/gwei.py b/src/lean_spec/types/gwei.py new file mode 100644 index 000000000..1df617e6f --- /dev/null +++ b/src/lean_spec/types/gwei.py @@ -0,0 +1,6 @@ +"""Epoch Type Specification.""" + +from .uint import Uint64 + +Gwei = Uint64 +"""A type alias to represent Gwei""" diff --git a/src/lean_spec/types/staker.py b/src/lean_spec/types/staker.py new file mode 100644 index 000000000..4ca05c4e7 --- /dev/null +++ b/src/lean_spec/types/staker.py @@ -0,0 +1,6 @@ +"""Staker-related type definitions for the specification.""" + +from .uint import Uint64 + +StakerIndex = Uint64 +"""A type alias for a staker's index in the registry.""" diff --git a/tests/lean_spec/subspecs/containers/test_state.py b/tests/lean_spec/subspecs/containers/test_state.py index bfa5eea20..5d8174325 100644 --- a/tests/lean_spec/subspecs/containers/test_state.py +++ b/tests/lean_spec/subspecs/containers/test_state.py @@ -17,7 +17,7 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.vote import SignedVote, Vote from lean_spec.subspecs.ssz import hash_tree_root -from lean_spec.types import Boolean, Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Boolean, Bytes32, StakerIndex, Uint64 @pytest.fixture @@ -87,7 +87,7 @@ def _create_block( # Construct the inner block message with correct parent_root linkage. block_message = Block( slot=Slot(slot), - proposer_index=ValidatorIndex(slot % 10), # Using sample_config num_validators + proposer_index=StakerIndex(slot % 10), # Using sample_config num_validators parent_root=hash_tree_root(parent_header), state_root=Bytes32.zero(), # Placeholder, to be filled in by STF body=body, @@ -108,10 +108,10 @@ def _create_votes(indices: List[int]) -> List[Boolean]: Returns ------- list[Boolean] - A bitlist of length VALIDATOR_REGISTRY_LIMIT with True at given indices. + A bitlist of length STAKER_REGISTRY_LIMIT with True at given indices. """ # Start with an all-false bitlist at registry-limit length. - votes = [Boolean(False)] * DEVNET_CONFIG.validator_registry_limit.as_int() + votes = [Boolean(False)] * DEVNET_CONFIG.staker_registry_limit.as_int() # Flip the positions listed in indices to True. for i in indices: votes[i] = Boolean(True) @@ -180,6 +180,7 @@ def base_state( return State( config=sample_config, slot=Slot(0), + stakers=[], latest_block_header=sample_block_header, latest_justified=sample_checkpoint, latest_finalized=sample_checkpoint, @@ -223,7 +224,7 @@ def test_get_justifications_single_root(base_state: State) -> None: root1 = Bytes32(b"\x01" * 32) # Prepare a vote bitlist with required length; flip two positions to True. - votes1 = [Boolean(False)] * DEVNET_CONFIG.validator_registry_limit.as_int() + votes1 = [Boolean(False)] * DEVNET_CONFIG.staker_registry_limit.as_int() votes1[2] = Boolean(True) # Validator 2 voted votes1[5] = Boolean(True) # Validator 5 voted @@ -259,7 +260,7 @@ def test_get_justifications_multiple_roots(base_state: State) -> None: root3 = Bytes32(b"\x03" * 32) # Validator registry limit length for each vote slice. - limit = DEVNET_CONFIG.validator_registry_limit.as_int() + limit = DEVNET_CONFIG.staker_registry_limit.as_int() # Build per-root vote slices. votes1 = [Boolean(False)] * limit @@ -308,13 +309,14 @@ def test_with_justifications_empty( initial_state = State( config=sample_config, slot=Slot(0), + stakers=[], latest_block_header=sample_block_header, latest_justified=sample_checkpoint, latest_finalized=sample_checkpoint, historical_block_hashes=[], justified_slots=[], justifications_roots=[Bytes32(b"\x01" * 32)], - justifications_validators=[True] * DEVNET_CONFIG.validator_registry_limit.as_int(), + justifications_validators=[True] * DEVNET_CONFIG.staker_registry_limit.as_int(), ) # Apply an empty justifications map to get a new state snapshot. @@ -344,7 +346,7 @@ def test_with_justifications_deterministic_order(base_state: State) -> None: root2 = Bytes32(b"\x02" * 32) # Build two vote slices of proper length. - limit = DEVNET_CONFIG.validator_registry_limit.as_int() + limit = DEVNET_CONFIG.staker_registry_limit.as_int() votes1 = [Boolean(False)] * limit votes2 = [Boolean(True)] * limit @@ -375,7 +377,7 @@ def test_with_justifications_invalid_length(base_state: State) -> None: root1 = Bytes32(b"\x01" * 32) # Construct an invalid votes bitlist: one short of required length. - invalid_votes = [Boolean(True)] * (DEVNET_CONFIG.validator_registry_limit - Uint64(1)).as_int() + invalid_votes = [Boolean(True)] * (DEVNET_CONFIG.staker_registry_limit - Uint64(1)).as_int() justifications = {root1: invalid_votes} # The method asserts on incorrect lengths. @@ -405,7 +407,7 @@ def test_with_justifications_invalid_length(base_state: State) -> None: pytest.param( { Bytes32(b"\x03" * 32): [Boolean(True)] - * DEVNET_CONFIG.validator_registry_limit.as_int(), + * DEVNET_CONFIG.staker_registry_limit.as_int(), Bytes32(b"\x01" * 32): _create_votes([0]), Bytes32(b"\x02" * 32): _create_votes([1, 2]), }, @@ -586,7 +588,7 @@ def test_process_block_header_invalid( # Build a block with possibly invalid slot, proposer, or parent root. block = Block( slot=Slot(bad_slot), - proposer_index=ValidatorIndex(bad_proposer), + proposer_index=StakerIndex(bad_proposer), parent_root=bad_parent_root or parent_root, state_root=Bytes32.zero(), body=BlockBody(attestations=[]), @@ -638,7 +640,7 @@ def test_process_attestations_justification_and_finalization(genesis_state: Stat votes_for_4 = [ SignedVote( data=Vote( - validator_id=ValidatorIndex(i), + validator_id=StakerIndex(i), slot=Slot(4), head=checkpoint4, target=checkpoint4, diff --git a/tests/lean_spec/subspecs/forkchoice/test_attestation_processing.py b/tests/lean_spec/subspecs/forkchoice/test_attestation_processing.py index 8c6f2bd18..c39263c46 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_attestation_processing.py +++ b/tests/lean_spec/subspecs/forkchoice/test_attestation_processing.py @@ -13,7 +13,7 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex, Uint64 @pytest.fixture @@ -67,7 +67,7 @@ def test_validate_attestation_valid(self, sample_store: Store) -> None: # Create valid signed vote vote = Vote( - validator_id=ValidatorIndex(0), + validator_id=StakerIndex(0), slot=Slot(2), head=Checkpoint(root=target_hash, slot=Slot(2)), target=Checkpoint(root=target_hash, slot=Slot(2)), @@ -105,7 +105,7 @@ def test_validate_attestation_slot_order_invalid(self, sample_store: Store) -> N # Create invalid signed vote (source > target slot) vote = Vote( - validator_id=ValidatorIndex(0), + validator_id=StakerIndex(0), slot=Slot(2), head=Checkpoint(root=target_hash, slot=Slot(1)), source=Checkpoint(root=source_hash, slot=Slot(2)), @@ -124,7 +124,7 @@ def test_validate_attestation_missing_blocks(self, sample_store: Store) -> None: # Create signed vote referencing missing blocks vote = Vote( - validator_id=ValidatorIndex(0), + validator_id=StakerIndex(0), slot=Slot(2), head=Checkpoint(root=target_hash, slot=Slot(2)), source=Checkpoint(root=source_hash, slot=Slot(1)), @@ -163,7 +163,7 @@ def test_validate_attestation_checkpoint_slot_mismatch(self, sample_store: Store # Create signed vote with mismatched checkpoint slot vote = Vote( - validator_id=ValidatorIndex(0), + validator_id=StakerIndex(0), slot=Slot(2), head=Checkpoint(root=target_hash, slot=Slot(2)), source=Checkpoint(root=source_hash, slot=Slot(0)), # Wrong slot (should be 1) @@ -202,7 +202,7 @@ def test_validate_attestation_too_far_future(self, sample_store: Store) -> None: # Create signed vote for future slot vote = Vote( - validator_id=ValidatorIndex(0), + validator_id=StakerIndex(0), slot=Slot(1000), # Too far in future head=Checkpoint(root=target_hash, slot=Slot(1000)), source=Checkpoint(root=source_hash, slot=Slot(1)), @@ -245,7 +245,7 @@ def test_process_network_attestation(self, sample_store: Store) -> None: # Create valid signed vote vote = Vote( - validator_id=ValidatorIndex(5), + validator_id=StakerIndex(5), slot=Slot(2), head=Checkpoint(root=target_hash, slot=Slot(2)), source=Checkpoint(root=source_hash, slot=Slot(1)), @@ -257,8 +257,8 @@ def test_process_network_attestation(self, sample_store: Store) -> None: sample_store.process_attestation(signed_vote, is_from_block=False) # Vote should be added to new votes - assert ValidatorIndex(5) in sample_store.latest_new_votes - assert sample_store.latest_new_votes[ValidatorIndex(5)] == vote.target + assert StakerIndex(5) in sample_store.latest_new_votes + assert sample_store.latest_new_votes[StakerIndex(5)] == vote.target def test_process_block_attestation(self, sample_store: Store) -> None: """Test processing attestation from a block.""" @@ -287,7 +287,7 @@ def test_process_block_attestation(self, sample_store: Store) -> None: # Create valid signed vote vote = Vote( - validator_id=ValidatorIndex(7), + validator_id=StakerIndex(7), slot=Slot(2), head=Checkpoint(root=target_hash, slot=Slot(2)), source=Checkpoint(root=source_hash, slot=Slot(1)), @@ -299,8 +299,8 @@ def test_process_block_attestation(self, sample_store: Store) -> None: sample_store.process_attestation(signed_vote, is_from_block=True) # Vote should be added to known votes - assert ValidatorIndex(7) in sample_store.latest_known_votes - assert sample_store.latest_known_votes[ValidatorIndex(7)] == vote.target + assert StakerIndex(7) in sample_store.latest_known_votes + assert sample_store.latest_known_votes[StakerIndex(7)] == vote.target def test_process_attestation_superseding(self, sample_store: Store) -> None: """Test that newer attestations supersede older ones.""" @@ -327,7 +327,7 @@ def test_process_attestation_superseding(self, sample_store: Store) -> None: sample_store.blocks[target_hash_1] = target_block_1 sample_store.blocks[target_hash_2] = target_block_2 - validator = ValidatorIndex(10) + validator = StakerIndex(10) # Process first (older) attestation vote_1 = Vote( @@ -380,7 +380,7 @@ def test_process_attestation_from_block_supersedes_new(self, sample_store: Store sample_store.blocks[source_hash] = source_block sample_store.blocks[target_hash] = target_block - validator = ValidatorIndex(15) + validator = StakerIndex(15) # First process as network vote vote = Vote( @@ -425,7 +425,7 @@ def test_process_block_with_attestations(self, sample_store: Store) -> None: # Create a vote that will be included in block vote = Vote( - validator_id=ValidatorIndex(20), + validator_id=StakerIndex(20), slot=Slot(2), head=Checkpoint(root=parent_hash, slot=Slot(1)), source=Checkpoint(root=parent_hash, slot=Slot(1)), @@ -437,6 +437,6 @@ def test_process_block_with_attestations(self, sample_store: Store) -> None: sample_store.process_attestation(signed_vote, is_from_block=True) # Verify the vote was processed correctly - assert ValidatorIndex(20) == vote.validator_id + assert StakerIndex(20) == vote.validator_id assert vote.target.root == parent_hash - assert ValidatorIndex(20) in sample_store.latest_known_votes + assert StakerIndex(20) in sample_store.latest_known_votes diff --git a/tests/lean_spec/subspecs/forkchoice/test_fork_choice_algorithm.py b/tests/lean_spec/subspecs/forkchoice/test_fork_choice_algorithm.py index ef77d1fb3..0b938aa43 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_fork_choice_algorithm.py +++ b/tests/lean_spec/subspecs/forkchoice/test_fork_choice_algorithm.py @@ -14,7 +14,7 @@ from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.forkchoice.helpers import get_fork_choice_head from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex, Uint64 @pytest.fixture @@ -75,7 +75,7 @@ def test_fork_choice_single_vote(self, sample_blocks: Dict[Bytes32, Block]) -> N root_hash = list(sample_blocks.keys())[0] target_hash = list(sample_blocks.keys())[2] # block_b - votes = {ValidatorIndex(0): Checkpoint(root=target_hash, slot=Slot(2))} + votes = {StakerIndex(0): Checkpoint(root=target_hash, slot=Slot(2))} head = get_fork_choice_head( blocks=sample_blocks, @@ -147,9 +147,9 @@ def test_fork_choice_with_multiple_forks(self) -> None: # More votes for fork 2 (C->D) votes = { - ValidatorIndex(0): Checkpoint(root=block_d_hash, slot=Slot(2)), - ValidatorIndex(1): Checkpoint(root=block_d_hash, slot=Slot(2)), - ValidatorIndex(2): Checkpoint( + StakerIndex(0): Checkpoint(root=block_d_hash, slot=Slot(2)), + StakerIndex(1): Checkpoint(root=block_d_hash, slot=Slot(2)), + StakerIndex(2): Checkpoint( root=block_b_hash, slot=Slot(2) ), # Single vote for fork 1 } @@ -203,8 +203,8 @@ def test_fork_choice_competing_votes(self) -> None: # Equal votes for both forks votes = { - ValidatorIndex(0): Checkpoint(root=block_a_hash, slot=Slot(1)), - ValidatorIndex(1): Checkpoint(root=block_b_hash, slot=Slot(1)), + StakerIndex(0): Checkpoint(root=block_a_hash, slot=Slot(1)), + StakerIndex(1): Checkpoint(root=block_b_hash, slot=Slot(1)), } head = get_fork_choice_head( @@ -284,7 +284,7 @@ def test_fork_choice_deep_chain(self) -> None: # Vote for the head block head_hash = prev_hash - votes = {ValidatorIndex(0): Checkpoint(root=head_hash, slot=Slot(9))} + votes = {StakerIndex(0): Checkpoint(root=head_hash, slot=Slot(9))} # Should find the head result = get_fork_choice_head( @@ -344,7 +344,7 @@ def test_fork_choice_ancestor_votes(self) -> None: # Vote for ancestor should still find the head votes = { - ValidatorIndex(0): Checkpoint(root=block_a_hash, slot=Slot(1)), + StakerIndex(0): Checkpoint(root=block_a_hash, slot=Slot(1)), } head = get_fork_choice_head( @@ -383,7 +383,7 @@ def test_fork_choice_with_min_score(self) -> None: } # Single vote shouldn't meet min_score of 2 - votes = {ValidatorIndex(0): Checkpoint(root=block_a_hash, slot=Slot(1))} + votes = {StakerIndex(0): Checkpoint(root=block_a_hash, slot=Slot(1))} head = get_fork_choice_head( blocks=blocks, diff --git a/tests/lean_spec/subspecs/forkchoice/test_helpers.py b/tests/lean_spec/subspecs/forkchoice/test_helpers.py index b310cb1fc..835651651 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_helpers.py +++ b/tests/lean_spec/subspecs/forkchoice/test_helpers.py @@ -11,7 +11,7 @@ get_latest_justified, ) from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex, Uint64 @pytest.fixture @@ -60,7 +60,7 @@ def test_get_fork_choice_head_with_votes(self, sample_blocks: Dict[Bytes32, Bloc target_hash = list(sample_blocks.keys())[2] # block_b # Create votes pointing to target - votes = {ValidatorIndex(0): Checkpoint(root=target_hash, slot=Slot(2))} + votes = {StakerIndex(0): Checkpoint(root=target_hash, slot=Slot(2))} head = get_fork_choice_head( blocks=sample_blocks, root=root_hash, latest_votes=votes, min_score=0 @@ -84,7 +84,7 @@ def test_get_fork_choice_head_with_min_score(self, sample_blocks: Dict[Bytes32, target_hash = list(sample_blocks.keys())[2] # block_b # Single vote, but require min_score of 2 - votes = {ValidatorIndex(0): Checkpoint(root=target_hash, slot=Slot(2))} + votes = {StakerIndex(0): Checkpoint(root=target_hash, slot=Slot(2))} head = get_fork_choice_head( blocks=sample_blocks, root=root_hash, latest_votes=votes, min_score=2 @@ -100,9 +100,9 @@ def test_get_fork_choice_head_multiple_votes(self, sample_blocks: Dict[Bytes32, # Multiple votes for same target votes = { - ValidatorIndex(0): Checkpoint(root=target_hash, slot=Slot(2)), - ValidatorIndex(1): Checkpoint(root=target_hash, slot=Slot(2)), - ValidatorIndex(2): Checkpoint(root=target_hash, slot=Slot(2)), + StakerIndex(0): Checkpoint(root=target_hash, slot=Slot(2)), + StakerIndex(1): Checkpoint(root=target_hash, slot=Slot(2)), + StakerIndex(2): Checkpoint(root=target_hash, slot=Slot(2)), } head = get_fork_choice_head( diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_lifecycle.py b/tests/lean_spec/subspecs/forkchoice/test_store_lifecycle.py index d771ca1d9..a2a31f67c 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_store_lifecycle.py +++ b/tests/lean_spec/subspecs/forkchoice/test_store_lifecycle.py @@ -11,7 +11,7 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex, Uint64 @pytest.fixture @@ -75,8 +75,8 @@ def test_store_initialization_with_data(self) -> None: latest_finalized=checkpoint, blocks={block_hash: block}, states={}, - latest_known_votes={ValidatorIndex(0): checkpoint}, - latest_new_votes={ValidatorIndex(1): checkpoint}, + latest_known_votes={StakerIndex(0): checkpoint}, + latest_new_votes={StakerIndex(1): checkpoint}, ) assert store.time == Uint64(200) @@ -85,8 +85,8 @@ def test_store_initialization_with_data(self) -> None: assert store.safe_target == block_hash assert block_hash in store.blocks assert store.blocks[block_hash] == block - assert ValidatorIndex(0) in store.latest_known_votes - assert ValidatorIndex(1) in store.latest_new_votes + assert StakerIndex(0) in store.latest_known_votes + assert StakerIndex(1) in store.latest_new_votes def test_store_factory_method(self) -> None: """Test Store.get_forkchoice_store factory method.""" @@ -110,6 +110,7 @@ def test_store_factory_method(self) -> None: state = State( config=config, slot=Slot(0), + stakers=[], latest_block_header=block_header, latest_justified=checkpoint, latest_finalized=checkpoint, diff --git a/tests/lean_spec/subspecs/forkchoice/test_time_management.py b/tests/lean_spec/subspecs/forkchoice/test_time_management.py index e0c6f0eb1..cca99e2a7 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_time_management.py +++ b/tests/lean_spec/subspecs/forkchoice/test_time_management.py @@ -11,7 +11,7 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex, Uint64 @pytest.fixture @@ -123,7 +123,7 @@ def test_tick_interval_actions_by_phase(self, sample_store: Store) -> None: # Add some test votes for processing test_checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(1)) - sample_store.latest_new_votes[ValidatorIndex(0)] = test_checkpoint + sample_store.latest_new_votes[StakerIndex(0)] = test_checkpoint # Tick through a complete slot cycle for interval in range(INTERVALS_PER_SLOT.as_int()): @@ -206,7 +206,7 @@ def test_accept_new_votes_basic(self, sample_store: Store) -> None: """Test basic new vote processing.""" # Add some new votes checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(1)) - sample_store.latest_new_votes[ValidatorIndex(0)] = checkpoint + sample_store.latest_new_votes[StakerIndex(0)] = checkpoint initial_new_votes = len(sample_store.latest_new_votes) initial_known_votes = len(sample_store.latest_known_votes) @@ -229,7 +229,7 @@ def test_accept_new_votes_multiple(self, sample_store: Store) -> None: ] for i, checkpoint in enumerate(checkpoints): - sample_store.latest_new_votes[ValidatorIndex(i)] = checkpoint + sample_store.latest_new_votes[StakerIndex(i)] = checkpoint # Accept all new votes sample_store.accept_new_votes() @@ -240,7 +240,7 @@ def test_accept_new_votes_multiple(self, sample_store: Store) -> None: # Verify correct mapping for i, checkpoint in enumerate(checkpoints): - assert sample_store.latest_known_votes[ValidatorIndex(i)] == checkpoint + assert sample_store.latest_known_votes[StakerIndex(i)] == checkpoint def test_accept_new_votes_empty(self, sample_store: Store) -> None: """Test accepting new votes when there are none.""" @@ -295,14 +295,14 @@ def test_get_proposal_head_processes_votes(self, sample_store: Store) -> None: """Test that get_proposal_head processes pending votes.""" # Add some new votes checkpoint = Checkpoint(root=Bytes32(b"vote" + b"\x00" * 28), slot=Slot(1)) - sample_store.latest_new_votes[ValidatorIndex(10)] = checkpoint + sample_store.latest_new_votes[StakerIndex(10)] = checkpoint # Get proposal head should process votes sample_store.get_proposal_head(Slot(1)) # Votes should have been processed (moved to known votes) - assert ValidatorIndex(10) not in sample_store.latest_new_votes - assert ValidatorIndex(10) in sample_store.latest_known_votes + assert StakerIndex(10) not in sample_store.latest_new_votes + assert StakerIndex(10) in sample_store.latest_known_votes class TestTimeConstants: diff --git a/tests/lean_spec/subspecs/forkchoice/test_validator.py b/tests/lean_spec/subspecs/forkchoice/test_validator.py index 1f3fe69e7..f8e91454b 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_validator.py +++ b/tests/lean_spec/subspecs/forkchoice/test_validator.py @@ -14,7 +14,7 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex, Uint64 from lean_spec.types.validator import is_proposer @@ -30,7 +30,7 @@ def sample_state(config: Config) -> State: # Create block header for testing block_header = BlockHeader( slot=Slot(0), - proposer_index=ValidatorIndex(0), + proposer_index=StakerIndex(0), parent_root=Bytes32.zero(), state_root=Bytes32(b"state" + b"\x00" * 27), body_root=Bytes32(b"body" + b"\x00" * 28), @@ -42,6 +42,7 @@ def sample_state(config: Config) -> State: return State( config=config, slot=Slot(0), + stakers=[], latest_block_header=block_header, latest_justified=temp_finalized, latest_finalized=temp_finalized, @@ -58,7 +59,7 @@ def sample_store(config: Config, sample_state: State) -> Store: # Create genesis block genesis_block = Block( slot=Slot(0), - proposer_index=ValidatorIndex(0), + proposer_index=StakerIndex(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(sample_state), body=BlockBody(attestations=[]), @@ -104,7 +105,7 @@ class TestBlockProduction: def test_produce_block_basic(self, sample_store: Store) -> None: """Test basic block production by authorized proposer.""" slot = Slot(1) - validator_idx = ValidatorIndex(1) # Proposer for slot 1 + validator_idx = StakerIndex(1) # Proposer for slot 1 block = sample_store.produce_block(slot, validator_idx) @@ -123,7 +124,7 @@ def test_produce_block_basic(self, sample_store: Store) -> None: def test_produce_block_unauthorized_proposer(self, sample_store: Store) -> None: """Test block production fails for unauthorized proposer.""" slot = Slot(1) - wrong_validator = ValidatorIndex(2) # Not proposer for slot 1 + wrong_validator = StakerIndex(2) # Not proposer for slot 1 with pytest.raises(AssertionError, match="is not the proposer for slot"): sample_store.produce_block(slot, wrong_validator) @@ -134,11 +135,11 @@ def test_produce_block_with_attestations(self, sample_store: Store) -> None: vote1 = Checkpoint(root=sample_store.head, slot=Slot(0)) vote2 = Checkpoint(root=sample_store.head, slot=Slot(0)) - sample_store.latest_known_votes[ValidatorIndex(5)] = vote1 - sample_store.latest_known_votes[ValidatorIndex(6)] = vote2 + sample_store.latest_known_votes[StakerIndex(5)] = vote1 + sample_store.latest_known_votes[StakerIndex(6)] = vote2 slot = Slot(2) - validator_idx = ValidatorIndex(2) # Proposer for slot 2 + validator_idx = StakerIndex(2) # Proposer for slot 2 block = sample_store.produce_block(slot, validator_idx) @@ -153,12 +154,12 @@ def test_produce_block_with_attestations(self, sample_store: Store) -> None: def test_produce_block_sequential_slots(self, sample_store: Store) -> None: """Test producing blocks in sequential slots.""" # Produce block for slot 1 - block1 = sample_store.produce_block(Slot(1), ValidatorIndex(1)) + block1 = sample_store.produce_block(Slot(1), StakerIndex(1)) block1_hash = hash_tree_root(block1) # Verify first block is properly created assert block1.slot == Slot(1) - assert block1.proposer_index == ValidatorIndex(1) + assert block1.proposer_index == StakerIndex(1) assert block1_hash in sample_store.blocks assert block1_hash in sample_store.states @@ -167,11 +168,11 @@ def test_produce_block_sequential_slots(self, sample_store: Store) -> None: # So block2 should build on genesis, not block1 # Produce block for slot 2 (will build on genesis due to forkchoice) - block2 = sample_store.produce_block(Slot(2), ValidatorIndex(2)) + block2 = sample_store.produce_block(Slot(2), StakerIndex(2)) # Verify block properties assert block2.slot == Slot(2) - assert block2.proposer_index == ValidatorIndex(2) + assert block2.proposer_index == StakerIndex(2) # The parent should be genesis (the current head), not block1 genesis_hash = sample_store.head @@ -186,7 +187,7 @@ def test_produce_block_sequential_slots(self, sample_store: Store) -> None: def test_produce_block_empty_attestations(self, sample_store: Store) -> None: """Test block production with no available attestations.""" slot = Slot(3) - validator_idx = ValidatorIndex(3) + validator_idx = StakerIndex(3) # Ensure no votes in store sample_store.latest_known_votes.clear() @@ -202,11 +203,11 @@ def test_produce_block_empty_attestations(self, sample_store: Store) -> None: def test_produce_block_state_consistency(self, sample_store: Store) -> None: """Test that produced block's state is consistent with block content.""" slot = Slot(4) - validator_idx = ValidatorIndex(4) + validator_idx = StakerIndex(4) # Add some votes to test state computation vote = Checkpoint(root=sample_store.head, slot=Slot(0)) - sample_store.latest_known_votes[ValidatorIndex(7)] = vote + sample_store.latest_known_votes[StakerIndex(7)] = vote block = sample_store.produce_block(slot, validator_idx) block_hash = hash_tree_root(block) @@ -222,7 +223,7 @@ class TestAttestationVoteProduction: def test_produce_attestation_vote_basic(self, sample_store: Store) -> None: """Test basic attestation vote production.""" slot = Slot(1) - validator_idx = ValidatorIndex(5) + validator_idx = StakerIndex(5) vote = sample_store.produce_attestation_vote(slot, validator_idx) @@ -239,7 +240,7 @@ def test_produce_attestation_vote_basic(self, sample_store: Store) -> None: def test_produce_attestation_vote_head_reference(self, sample_store: Store) -> None: """Test that attestation vote references correct head.""" slot = Slot(2) - validator_idx = ValidatorIndex(8) + validator_idx = StakerIndex(8) vote = sample_store.produce_attestation_vote(slot, validator_idx) @@ -254,7 +255,7 @@ def test_produce_attestation_vote_head_reference(self, sample_store: Store) -> N def test_produce_attestation_vote_target_calculation(self, sample_store: Store) -> None: """Test that attestation vote calculates target correctly.""" slot = Slot(3) - validator_idx = ValidatorIndex(9) + validator_idx = StakerIndex(9) vote = sample_store.produce_attestation_vote(slot, validator_idx) @@ -270,11 +271,11 @@ def test_produce_attestation_vote_different_validators(self, sample_store: Store # All validators should produce consistent votes for the same slot votes = [] for validator_idx in range(5): - vote = sample_store.produce_attestation_vote(slot, ValidatorIndex(validator_idx)) + vote = sample_store.produce_attestation_vote(slot, StakerIndex(validator_idx)) votes.append(vote) # Each vote should have correct validator ID - assert vote.validator_id == ValidatorIndex(validator_idx) + assert vote.validator_id == StakerIndex(validator_idx) assert vote.slot == slot # All votes should have same head, target, and source (consensus) @@ -289,7 +290,7 @@ def test_produce_attestation_vote_different_validators(self, sample_store: Store def test_produce_attestation_vote_sequential_slots(self, sample_store: Store) -> None: """Test vote production across sequential slots.""" - validator_idx = ValidatorIndex(3) + validator_idx = StakerIndex(3) # Produce votes for sequential slots vote1 = sample_store.produce_attestation_vote(Slot(1), validator_idx) @@ -306,7 +307,7 @@ def test_produce_attestation_vote_sequential_slots(self, sample_store: Store) -> def test_produce_attestation_vote_justification_consistency(self, sample_store: Store) -> None: """Test that vote source uses current justified checkpoint.""" slot = Slot(5) - validator_idx = ValidatorIndex(2) + validator_idx = StakerIndex(2) vote = sample_store.produce_attestation_vote(slot, validator_idx) @@ -325,7 +326,7 @@ def test_block_production_then_attestation(self, sample_store: Store) -> None: """Test producing a block then creating attestation for it.""" # Proposer produces block for slot 1 proposer_slot = Slot(1) - proposer_idx = ValidatorIndex(1) + proposer_idx = StakerIndex(1) sample_store.produce_block(proposer_slot, proposer_idx) # Update store state after block production @@ -333,7 +334,7 @@ def test_block_production_then_attestation(self, sample_store: Store) -> None: # Other validator creates attestation for slot 2 attestor_slot = Slot(2) - attestor_idx = ValidatorIndex(7) + attestor_idx = StakerIndex(7) vote = sample_store.produce_attestation_vote(attestor_slot, attestor_idx) # Vote should reference the new block as head (if it became head) @@ -346,14 +347,14 @@ def test_block_production_then_attestation(self, sample_store: Store) -> None: def test_multiple_validators_coordination(self, sample_store: Store) -> None: """Test multiple validators producing blocks and attestations.""" # Validator 1 produces block for slot 1 - block1 = sample_store.produce_block(Slot(1), ValidatorIndex(1)) + block1 = sample_store.produce_block(Slot(1), StakerIndex(1)) block1_hash = hash_tree_root(block1) # Validators 2-5 create attestations for slot 2 # These will be based on the current forkchoice head (genesis) attestations = [] for i in range(2, 6): - vote = sample_store.produce_attestation_vote(Slot(2), ValidatorIndex(i)) + vote = sample_store.produce_attestation_vote(Slot(2), StakerIndex(i)) attestations.append(vote) # All attestations should be consistent @@ -365,11 +366,11 @@ def test_multiple_validators_coordination(self, sample_store: Store) -> None: # Validator 2 produces next block for slot 2 # Without votes for block1, this will build on genesis (current head) - block2 = sample_store.produce_block(Slot(2), ValidatorIndex(2)) + block2 = sample_store.produce_block(Slot(2), StakerIndex(2)) # Verify block properties assert block2.slot == Slot(2) - assert block2.proposer_index == ValidatorIndex(2) + assert block2.proposer_index == StakerIndex(2) # Both blocks should exist in the store block2_hash = hash_tree_root(block2) @@ -384,7 +385,7 @@ def test_multiple_validators_coordination(self, sample_store: Store) -> None: def test_validator_edge_cases(self, sample_store: Store) -> None: """Test edge cases in validator operations.""" # Test with validator index equal to number of validators - 1 - max_validator = ValidatorIndex(9) # Last validator (0-indexed, 10 total) + max_validator = StakerIndex(9) # Last validator (0-indexed, 10 total) slot = Slot(9) # This validator's slot # Should be able to produce block @@ -407,9 +408,10 @@ def test_validator_operations_empty_store(self) -> None: state = State( config=config, slot=Slot(0), + stakers=[], latest_block_header=BlockHeader( slot=Slot(0), - proposer_index=ValidatorIndex(0), + proposer_index=StakerIndex(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), # Will be updated body_root=hash_tree_root(genesis_body), @@ -428,7 +430,7 @@ def test_validator_operations_empty_store(self) -> None: # Create genesis block with correct state root genesis = Block( slot=Slot(0), - proposer_index=ValidatorIndex(0), + proposer_index=StakerIndex(0), parent_root=Bytes32.zero(), state_root=state_root, body=genesis_body, @@ -438,7 +440,7 @@ def test_validator_operations_empty_store(self) -> None: # Update state with matching header and checkpoint consistent_header = BlockHeader( slot=Slot(0), - proposer_index=ValidatorIndex(0), + proposer_index=StakerIndex(0), parent_root=Bytes32.zero(), state_root=state_root, # Same as block body_root=hash_tree_root(genesis_body), @@ -465,8 +467,8 @@ def test_validator_operations_empty_store(self) -> None: ) # Should be able to produce block and attestation - block = store.produce_block(Slot(1), ValidatorIndex(1)) - vote = store.produce_attestation_vote(Slot(1), ValidatorIndex(2)) + block = store.produce_block(Slot(1), StakerIndex(1)) + vote = store.produce_attestation_vote(Slot(1), StakerIndex(2)) assert isinstance(block, Block) assert isinstance(vote, Vote) @@ -478,7 +480,7 @@ class TestValidatorErrorHandling: def test_produce_block_wrong_proposer(self, sample_store: Store) -> None: """Test error when wrong validator tries to produce block.""" slot = Slot(5) - wrong_proposer = ValidatorIndex(3) # Should be validator 5 for slot 5 + wrong_proposer = StakerIndex(3) # Should be validator 5 for slot 5 with pytest.raises(AssertionError) as exc_info: sample_store.produce_block(slot, wrong_proposer) @@ -503,15 +505,15 @@ def test_produce_block_missing_parent_state(self) -> None: ) with pytest.raises(KeyError): # Missing head in get_proposal_head - store.produce_block(Slot(1), ValidatorIndex(1)) + store.produce_block(Slot(1), StakerIndex(1)) def test_validator_operations_invalid_parameters(self, sample_store: Store) -> None: """Test validator operations with invalid parameters.""" # These should not raise errors but work with the given types - # since ValidatorIndex is just a Uint64 alias + # since StakerIndex is just a Uint64 alias # Very large validator index (should work mathematically) - large_validator = ValidatorIndex(1000000) + large_validator = StakerIndex(1000000) large_slot = Slot(1000000) # is_proposer should work (though likely return False) diff --git a/tests/lean_spec/subspecs/forkchoice/test_vote_target_selection.py b/tests/lean_spec/subspecs/forkchoice/test_vote_target_selection.py index 1fe56c9ff..a35017b44 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_vote_target_selection.py +++ b/tests/lean_spec/subspecs/forkchoice/test_vote_target_selection.py @@ -11,7 +11,7 @@ from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.forkchoice import Store from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.types import Bytes32, Uint64, ValidatorIndex +from lean_spec.types import Bytes32, StakerIndex, Uint64 @pytest.fixture @@ -334,8 +334,8 @@ def test_safe_target_with_votes(self, config: Config) -> None: # Add some new votes new_votes = { - ValidatorIndex(0): Checkpoint(root=block_1_hash, slot=Slot(1)), - ValidatorIndex(1): Checkpoint(root=block_1_hash, slot=Slot(1)), + StakerIndex(0): Checkpoint(root=block_1_hash, slot=Slot(1)), + StakerIndex(1): Checkpoint(root=block_1_hash, slot=Slot(1)), } store = Store( diff --git a/tests/lean_spec/types/test_validator_utils.py b/tests/lean_spec/types/test_validator_utils.py index fadc51aef..e6fd050a4 100644 --- a/tests/lean_spec/types/test_validator_utils.py +++ b/tests/lean_spec/types/test_validator_utils.py @@ -2,7 +2,7 @@ import pytest -from lean_spec.types import Uint64, ValidatorIndex, is_proposer +from lean_spec.types import StakerIndex, Uint64, is_proposer class TestIsProposer: @@ -18,16 +18,16 @@ def test_is_proposer_basic(self) -> None: num_validators = Uint64(10) # At slot 0, validator 0 should be the proposer (0 % 10 == 0) - assert is_proposer(ValidatorIndex(0), Uint64(0), num_validators) is True - assert is_proposer(ValidatorIndex(1), Uint64(0), num_validators) is False + assert is_proposer(StakerIndex(0), Uint64(0), num_validators) is True + assert is_proposer(StakerIndex(1), Uint64(0), num_validators) is False # At slot 7, validator 7 should be the proposer (7 % 10 == 7) - assert is_proposer(ValidatorIndex(7), Uint64(7), num_validators) is True - assert is_proposer(ValidatorIndex(8), Uint64(7), num_validators) is False + assert is_proposer(StakerIndex(7), Uint64(7), num_validators) is True + assert is_proposer(StakerIndex(8), Uint64(7), num_validators) is False # At slot 9, validator 9 should be the proposer (9 % 10 == 9) - assert is_proposer(ValidatorIndex(9), Uint64(9), num_validators) is True - assert is_proposer(ValidatorIndex(0), Uint64(9), num_validators) is False + assert is_proposer(StakerIndex(9), Uint64(9), num_validators) is True + assert is_proposer(StakerIndex(0), Uint64(9), num_validators) is False def test_is_proposer_wraparound(self) -> None: """ @@ -39,16 +39,16 @@ def test_is_proposer_wraparound(self) -> None: num_validators = Uint64(10) # At slot 10, wrap-around selects validator 0 (10 % 10 == 0) - assert is_proposer(ValidatorIndex(0), Uint64(10), num_validators) is True - assert is_proposer(ValidatorIndex(1), Uint64(10), num_validators) is False + assert is_proposer(StakerIndex(0), Uint64(10), num_validators) is True + assert is_proposer(StakerIndex(1), Uint64(10), num_validators) is False # At slot 23, wrap-around selects validator 3 (23 % 10 == 3) - assert is_proposer(ValidatorIndex(3), Uint64(23), num_validators) is True - assert is_proposer(ValidatorIndex(2), Uint64(23), num_validators) is False + assert is_proposer(StakerIndex(3), Uint64(23), num_validators) is True + assert is_proposer(StakerIndex(2), Uint64(23), num_validators) is False # At slot 100, wrap-around selects validator 0 (100 % 10 == 0) - assert is_proposer(ValidatorIndex(0), Uint64(100), num_validators) is True - assert is_proposer(ValidatorIndex(5), Uint64(100), num_validators) is False + assert is_proposer(StakerIndex(0), Uint64(100), num_validators) is True + assert is_proposer(StakerIndex(5), Uint64(100), num_validators) is False def test_is_proposer_large_numbers(self) -> None: """ @@ -59,14 +59,14 @@ def test_is_proposer_large_numbers(self) -> None: num_validators = Uint64(1000) # Test with large slot numbers - assert is_proposer(ValidatorIndex(555), Uint64(555), num_validators) is True - assert is_proposer(ValidatorIndex(556), Uint64(555), num_validators) is False + assert is_proposer(StakerIndex(555), Uint64(555), num_validators) is True + assert is_proposer(StakerIndex(556), Uint64(555), num_validators) is False # Test wrap-around with large numbers slot = Uint64(12345) - expected_proposer = ValidatorIndex(slot % num_validators) # 345 + expected_proposer = StakerIndex(slot % num_validators) # 345 assert is_proposer(expected_proposer, slot, num_validators) is True - assert is_proposer(ValidatorIndex(0), slot, num_validators) is False + assert is_proposer(StakerIndex(0), slot, num_validators) is False def test_is_proposer_single_validator(self) -> None: """ @@ -77,9 +77,9 @@ def test_is_proposer_single_validator(self) -> None: num_validators = Uint64(1) # With only one validator, they should always be the proposer - assert is_proposer(ValidatorIndex(0), Uint64(0), num_validators) is True - assert is_proposer(ValidatorIndex(0), Uint64(1), num_validators) is True - assert is_proposer(ValidatorIndex(0), Uint64(100), num_validators) is True + assert is_proposer(StakerIndex(0), Uint64(0), num_validators) is True + assert is_proposer(StakerIndex(0), Uint64(1), num_validators) is True + assert is_proposer(StakerIndex(0), Uint64(100), num_validators) is True def test_is_proposer_edge_cases(self) -> None: """ @@ -91,14 +91,14 @@ def test_is_proposer_edge_cases(self) -> None: num_validators = Uint64(3) # Test all validators in a 3-validator system - assert is_proposer(ValidatorIndex(0), Uint64(0), num_validators) is True - assert is_proposer(ValidatorIndex(1), Uint64(1), num_validators) is True - assert is_proposer(ValidatorIndex(2), Uint64(2), num_validators) is True + assert is_proposer(StakerIndex(0), Uint64(0), num_validators) is True + assert is_proposer(StakerIndex(1), Uint64(1), num_validators) is True + assert is_proposer(StakerIndex(2), Uint64(2), num_validators) is True # Test wrap-around in small system - assert is_proposer(ValidatorIndex(0), Uint64(3), num_validators) is True - assert is_proposer(ValidatorIndex(1), Uint64(4), num_validators) is True - assert is_proposer(ValidatorIndex(2), Uint64(5), num_validators) is True + assert is_proposer(StakerIndex(0), Uint64(3), num_validators) is True + assert is_proposer(StakerIndex(1), Uint64(4), num_validators) is True + assert is_proposer(StakerIndex(2), Uint64(5), num_validators) is True def test_is_proposer_validation(self) -> None: """ @@ -111,10 +111,10 @@ def test_is_proposer_validation(self) -> None: # Test that all non-proposer validators return False for slot_num in range(20): # Test multiple cycles slot = Uint64(slot_num) - expected_proposer = ValidatorIndex(slot_num % 5) + expected_proposer = StakerIndex(slot_num % 5) for validator_idx in range(5): - validator = ValidatorIndex(validator_idx) + validator = StakerIndex(validator_idx) expected_result = validator == expected_proposer actual_result = is_proposer(validator, slot, num_validators) @@ -132,13 +132,13 @@ def test_is_proposer_type_consistency(self) -> None: num_validators = Uint64(7) # Test with explicit type construction - validator = ValidatorIndex(3) + validator = StakerIndex(3) slot = Uint64(10) # 10 % 7 = 3 assert is_proposer(validator, slot, num_validators) is True # Test with different validator - other_validator = ValidatorIndex(4) + other_validator = StakerIndex(4) assert is_proposer(other_validator, slot, num_validators) is False @pytest.mark.parametrize("num_validators", [1, 2, 5, 10, 100, 1000]) @@ -153,12 +153,12 @@ def test_is_proposer_parametrized(self, num_validators: int) -> None: # Test first few slots for slot_num in range(min(20, num_validators * 2)): slot = Uint64(slot_num) - expected_proposer = ValidatorIndex(slot_num % num_validators) + expected_proposer = StakerIndex(slot_num % num_validators) # The expected proposer should return True assert is_proposer(expected_proposer, slot, validators) is True # A different validator should return False if num_validators > 1: - other_validator = ValidatorIndex((slot_num + 1) % num_validators) + other_validator = StakerIndex((slot_num + 1) % num_validators) assert is_proposer(other_validator, slot, validators) is False