Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/lean_spec/subspecs/containers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,5 @@ class Config(Container):
in the absence of more complex mechanisms like RANDAO or deposits.
"""

num_validators: Uint64
"""The total number of validators in the network."""

genesis_time: Uint64
"""The timestamp of the genesis block."""
15 changes: 7 additions & 8 deletions src/lean_spec/subspecs/containers/state/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,16 @@ class State(Container):
"""A bitlist of validators who participated in justifications."""

@classmethod
def generate_genesis(cls, genesis_time: Uint64, num_validators: Uint64) -> "State":
def generate_genesis(cls, genesis_time: Uint64, validators: Validators) -> "State":
"""
Generate a genesis state with empty history and proper initial values.

Parameters
----------
genesis_time : Uint64
The genesis timestamp.
num_validators : Uint64
The number of validators in the genesis state.
validators : Validators
The list of validators in the genesis state.

Returns:
-------
Expand All @@ -84,7 +84,6 @@ def generate_genesis(cls, genesis_time: Uint64, num_validators: Uint64) -> "Stat
"""
# Configure the genesis state.
genesis_config = Config(
num_validators=num_validators,
genesis_time=genesis_time,
)

Expand All @@ -106,7 +105,7 @@ def generate_genesis(cls, genesis_time: Uint64, num_validators: Uint64) -> "Stat
latest_finalized=Checkpoint.default(),
historical_block_hashes=HistoricalBlockHashes(data=[]),
justified_slots=JustifiedSlots(data=[]),
validators=Validators(data=[]),
validators=validators,
justifications_roots=JustificationRoots(data=[]),
justifications_validators=JustificationValidators(data=[]),
)
Expand All @@ -129,7 +128,7 @@ def is_proposer(self, validator_index: ValidatorIndex) -> bool:
return is_proposer(
validator_index=validator_index,
slot=self.slot,
num_validators=self.config.num_validators,
num_validators=Uint64(self.validators.count),
)

def get_justifications(self) -> Dict[Bytes32, List[Boolean]]:
Expand All @@ -152,7 +151,7 @@ def get_justifications(self) -> Dict[Bytes32, List[Boolean]]:
return justifications

# Compute the length of each validator vote slice.
validator_count = self.config.num_validators.as_int()
validator_count = self.validators.count

# Extract vote slices for each justified root.
flat_votes = list(self.justifications_validators)
Expand Down Expand Up @@ -195,7 +194,7 @@ def with_justifications(
for root in sorted(justifications.keys()):
votes = justifications[root]
# Validate that the vote list has the expected length.
expected_len = self.config.num_validators.as_int()
expected_len = self.validators.count
if len(votes) != expected_len:
raise AssertionError(f"Vote list for root {root.hex()} has incorrect length")

Expand Down
5 changes: 5 additions & 0 deletions src/lean_spec/subspecs/containers/state/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ class Validators(SSZList):

ELEMENT_TYPE = Validator
LIMIT = DEVNET_CONFIG.validator_registry_limit.as_int()

@property
def count(self) -> int:
"""Return the number of validators in the registry."""
return len(self.data)
17 changes: 11 additions & 6 deletions src/lean_spec/subspecs/forkchoice/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,12 @@ def update_safe_target(self) -> None:

Computes target that has sufficient (2/3+ majority) vote support.
"""
# Get validator count from head state
head_state = self.states[self.head]
num_validators = head_state.validators.count

# Calculate 2/3 majority threshold (ceiling division)
min_target_score = -(-self.config.num_validators * 2 // 3)
min_target_score = -(-num_validators * 2 // 3)

# Find head with minimum vote threshold
safe_target = get_fork_choice_head(
Expand Down Expand Up @@ -457,15 +461,16 @@ def produce_block_with_signatures(
Raises:
AssertionError: If validator lacks proposer authorization for slot
"""
# Validate proposer authorization for this slot
if not is_proposer(validator_index, slot, self.config.num_validators):
msg = f"Validator {validator_index} is not the proposer for slot {slot}"
raise AssertionError(msg)

# Get parent block and state to build upon
head_root = self.get_proposal_head(slot)
head_state = self.states[head_root]

# Validate proposer authorization for this slot
num_validators = Uint64(head_state.validators.count)
if not is_proposer(validator_index, slot, num_validators):
msg = f"Validator {validator_index} is not the proposer for slot {slot}"
raise AssertionError(msg)

# Initialize empty attestation set for iterative collection
attestations: list[Attestation] = []
signatures: list[Bytes4000] = []
Expand Down
42 changes: 26 additions & 16 deletions tests/lean_spec/subspecs/containers/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Config,
SignedAttestation,
State,
Validator,
)
from lean_spec.subspecs.containers.block import Attestations
from lean_spec.subspecs.containers.slot import Slot
Expand All @@ -26,7 +27,7 @@
Validators,
)
from lean_spec.subspecs.ssz import hash_tree_root
from lean_spec.types import Boolean, Bytes32, Bytes4000, Uint64, ValidatorIndex
from lean_spec.types import Boolean, Bytes32, Bytes52, Bytes4000, Uint64, ValidatorIndex


@pytest.fixture
Expand All @@ -37,11 +38,10 @@ def sample_config() -> Config:
Returns
-------
Config
A configuration with 4096 validators and genesis_time set to 0.
A configuration with genesis_time set to 0.
"""
# Create and return a simple configuration used across tests.
return Config(
num_validators=DEVNET_CONFIG.validator_registry_limit,
genesis_time=Uint64(0),
)

Expand All @@ -54,17 +54,20 @@ def genesis_state(sample_config: Config) -> State:
Parameters
----------
sample_config : Config
The configuration fixture with validator count and genesis time.
The configuration fixture with genesis time.

Returns
-------
State
A fresh genesis state produced by the class factory.
"""
# Call the canonical genesis factory with the sample configuration values.
# Create validators list with 4096 validators
num_validators = DEVNET_CONFIG.validator_registry_limit.as_int()
validators = Validators(data=[Validator(pubkey=Bytes52.zero()) for _ in range(num_validators)])
return State.generate_genesis(
genesis_time=sample_config.genesis_time,
num_validators=sample_config.num_validators,
validators=validators,
)


Expand Down Expand Up @@ -205,7 +208,7 @@ def base_state(
Parameters
----------
sample_config : Config
Test configuration with 10 validators.
Test configuration.
sample_block_header : BlockHeader
Zeroed header used as latest_block_header.
sample_checkpoint : Checkpoint
Expand All @@ -217,6 +220,9 @@ def base_state(
A State with empty history and justification lists.
"""
# Build a State with the provided fixtures and no history/justifications.
# Create validators list with registry limit validators
num_validators = DEVNET_CONFIG.validator_registry_limit.as_int()
validators = Validators(data=[Validator(pubkey=Bytes52.zero()) for _ in range(num_validators)])
return State(
config=sample_config,
slot=Slot(0),
Expand All @@ -227,7 +233,7 @@ def base_state(
justified_slots=JustifiedSlots(data=[]),
justifications_roots=JustificationRoots(data=[]),
justifications_validators=JustificationValidators(data=[]),
validators=Validators(data=[]),
validators=validators,
)


Expand Down Expand Up @@ -264,7 +270,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)] * base_state.config.num_validators.as_int()
votes1 = [Boolean(False)] * base_state.validators.count
votes1[2] = Boolean(True) # Validator 2 voted
votes1[5] = Boolean(True) # Validator 5 voted

Expand Down Expand Up @@ -300,7 +306,7 @@ def test_get_justifications_multiple_roots(base_state: State) -> None:
root3 = Bytes32(b"\x03" * 32)

# Validator count for each vote slice.
count = base_state.config.num_validators.as_int()
count = base_state.validators.count

# Build per-root vote slices.
votes1 = [Boolean(False)] * count
Expand Down Expand Up @@ -346,6 +352,9 @@ def test_with_justifications_empty(
- Ensure original state is unchanged.
"""
# Build a state populated with a single root and a full votes bitlist.
# Create validators list with registry limit validators
num_validators = DEVNET_CONFIG.validator_registry_limit.as_int()
validators = Validators(data=[Validator(pubkey=Bytes52.zero()) for _ in range(num_validators)])
initial_state = State(
config=sample_config,
slot=Slot(0),
Expand All @@ -355,10 +364,8 @@ def test_with_justifications_empty(
historical_block_hashes=HistoricalBlockHashes(data=[]),
justified_slots=JustifiedSlots(data=[]),
justifications_roots=JustificationRoots(data=[Bytes32(b"\x01" * 32)]),
justifications_validators=JustificationValidators(
data=[Boolean(True)] * sample_config.num_validators.as_int()
),
validators=Validators(data=[]),
justifications_validators=JustificationValidators(data=[Boolean(True)] * num_validators),
validators=validators,
)

# Apply an empty justifications map to get a new state snapshot.
Expand Down Expand Up @@ -388,7 +395,7 @@ def test_with_justifications_deterministic_order(base_state: State) -> None:
root2 = Bytes32(b"\x02" * 32)

# Build two vote slices of proper length.
count = base_state.config.num_validators.as_int()
count = base_state.validators.count
votes1 = [Boolean(False)] * count
votes2 = [Boolean(True)] * count
# Intentionally supply the dict in unsorted key order.
Expand Down Expand Up @@ -418,7 +425,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)] * (base_state.config.num_validators - Uint64(1)).as_int()
invalid_votes = [Boolean(True)] * (base_state.validators.count - 1)
justifications = {root1: invalid_votes}

# The method asserts on incorrect lengths.
Expand Down Expand Up @@ -492,9 +499,12 @@ def test_generate_genesis(sample_config: Config) -> None:
- Ensure historical/justification lists start empty.
"""
# Produce a genesis state from the sample config.
# Create validators list with registry limit validators
num_validators = DEVNET_CONFIG.validator_registry_limit.as_int()
validators = Validators(data=[Validator(pubkey=Bytes52.zero()) for _ in range(num_validators)])
state = State.generate_genesis(
genesis_time=sample_config.genesis_time,
num_validators=sample_config.num_validators,
validators=validators,
)

# Config in state should match the input.
Expand Down
1 change: 0 additions & 1 deletion tests/lean_spec/subspecs/forkchoice/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def __init__(self, latest_justified: Checkpoint) -> None:
"""Initialize a mock state with minimal defaults."""
# Create minimal defaults for all required fields
genesis_config = Config(
num_validators=Uint64(4),
genesis_time=Uint64(0),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
@pytest.fixture
def sample_config() -> Config:
"""Sample configuration for testing."""
return Config(num_validators=Uint64(100), genesis_time=Uint64(1000))
return Config(genesis_time=Uint64(1000))


@pytest.fixture
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ class TestStoreBasedForkChoice:
@pytest.fixture
def config(self) -> Config:
"""Sample configuration."""
return Config(genesis_time=Uint64(1000), num_validators=Uint64(100))
return Config(genesis_time=Uint64(1000))

def test_store_fork_choice_no_votes(self, config: Config) -> None:
"""Test Store.get_proposal_head with no votes."""
Expand Down
13 changes: 6 additions & 7 deletions tests/lean_spec/subspecs/forkchoice/test_store_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
@pytest.fixture
def sample_config() -> Config:
"""Sample configuration for testing."""
return Config(num_validators=Uint64(100), genesis_time=Uint64(1000))
return Config(genesis_time=Uint64(1000))


@pytest.fixture
Expand Down Expand Up @@ -57,15 +57,14 @@ class TestStoreCreation:
def test_store_creation_basic(self, sample_store: Store) -> None:
"""Test basic Store creation with required fields."""
assert sample_store.time == Uint64(100)
assert sample_store.config.num_validators == Uint64(100)
assert sample_store.head == Bytes32(b"head_root" + b"\x00" * 23)
assert sample_store.safe_target == Bytes32(b"safe_root" + b"\x00" * 23)
assert isinstance(sample_store.latest_justified, Checkpoint)
assert isinstance(sample_store.latest_finalized, Checkpoint)

def test_store_initialization_with_data(self) -> None:
"""Test Store initialization with blocks and states."""
config = Config(num_validators=Uint64(10), genesis_time=Uint64(2000))
config = Config(genesis_time=Uint64(2000))
checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(5))

# Sample block
Expand Down Expand Up @@ -112,7 +111,7 @@ def test_store_initialization_with_data(self) -> None:
def test_store_factory_method(self) -> None:
"""Test Store.get_forkchoice_store factory method."""

config = Config(num_validators=Uint64(10), genesis_time=Uint64(1000))
config = Config(genesis_time=Uint64(1000))
checkpoint = Checkpoint(root=Bytes32(b"genesis" + b"\x00" * 25), slot=Slot(0))

# Create block header for testing
Expand Down Expand Up @@ -166,7 +165,7 @@ class TestStoreDefaultValues:

def test_store_empty_collections_by_default(self) -> None:
"""Test that Store initializes empty collections by default."""
config = Config(num_validators=Uint64(5), genesis_time=Uint64(500))
config = Config(genesis_time=Uint64(500))
checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(0))

store = Store(
Expand Down Expand Up @@ -201,7 +200,7 @@ class TestStoreValidation:

def test_store_validation_required_fields(self) -> None:
"""Test that Store validates required fields."""
config = Config(num_validators=Uint64(5), genesis_time=Uint64(500))
config = Config(genesis_time=Uint64(500))
checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(0))

# Should create successfully with all required fields
Expand All @@ -223,7 +222,7 @@ def test_store_validation_required_fields(self) -> None:

def test_store_type_validation(self) -> None:
"""Test Store validates field types."""
config = Config(num_validators=Uint64(5), genesis_time=Uint64(500))
config = Config(genesis_time=Uint64(500))
checkpoint = Checkpoint(root=Bytes32(b"test" + b"\x00" * 28), slot=Slot(0))

# Test with wrong type for time - should work due to Pydantic coercion
Expand Down
Loading
Loading