Skip to content
Closed
9 changes: 4 additions & 5 deletions src/lean_spec/subspecs/chain/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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.
Expand All @@ -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,
)
2 changes: 2 additions & 0 deletions src/lean_spec/subspecs/containers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -14,6 +15,7 @@
"Config",
"SignedBlock",
"SignedVote",
"Staker",
"State",
"Vote",
]
2 changes: 1 addition & 1 deletion src/lean_spec/subspecs/containers/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
65 changes: 65 additions & 0 deletions src/lean_spec/subspecs/containers/staker.py
Original file line number Diff line number Diff line change
@@ -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),
]
Comment on lines +35 to +38
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to use List or Vector now that we have them.

See https://github.com/leanEthereum/leanSpec/blob/main/src/lean_spec/types/collections.py and usage over the codebase.

"""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.
"""
31 changes: 22 additions & 9 deletions src/lean_spec/subspecs/containers/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
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

from .block import Block, BlockBody, BlockHeader, SignedBlock
from .checkpoint import Checkpoint
from .config import Config
from .slot import Slot
from .staker import Staker
from .vote import SignedVote, Vote


Expand Down Expand Up @@ -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":
"""
Expand All @@ -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=[])),
Expand All @@ -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)),
Expand All @@ -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]]:
"""
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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()):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/lean_spec/subspecs/forkchoice/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
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


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:
"""
Expand Down
12 changes: 6 additions & 6 deletions src/lean_spec/subspecs/forkchoice/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
10 changes: 10 additions & 0 deletions src/lean_spec/subspecs/staker/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
23 changes: 23 additions & 0 deletions src/lean_spec/subspecs/staker/config.py
Original file line number Diff line number Diff line change
@@ -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."""
Loading
Loading