Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
63 changes: 61 additions & 2 deletions src/lean_spec/subspecs/containers/validator.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,76 @@
"""Validator container for the Lean Ethereum consensus specification."""

from lean_spec.types import Bytes52, Container
from __future__ import annotations

from typing import TYPE_CHECKING

from lean_spec.types import Bytes52, Container, ValidatorIndex

from ..xmss.containers import PublicKey
from ..xmss.interface import TEST_SIGNATURE_SCHEME, GeneralizedXmssScheme
from .attestation import Attestation, AttestationData
from .checkpoint import Checkpoint

if TYPE_CHECKING:
from ..forkchoice.store import Store
from .attestation import Attestation
from .slot import Slot


class Validator(Container):
"""Represents a validator's static metadata."""
"""Represents a validator's static metadata and operational interface."""

pubkey: Bytes52
"""XMSS one-time signature public key."""

index: ValidatorIndex = ValidatorIndex(0)
"""Validator index in the registry."""

def get_pubkey(self, scheme: GeneralizedXmssScheme = TEST_SIGNATURE_SCHEME) -> PublicKey:
"""Get the XMSS public key from this validator."""
return PublicKey.from_bytes(bytes(self.pubkey), scheme.config)

def produce_attestation(
self,
store: Store,
slot: Slot,
) -> Attestation:
"""
Produce an attestation for the given slot.

This method constructs an Attestation object according to the lean protocol
specification. The attestation represents the validator's view of the chain
state and their choice for the next justified checkpoint.

The algorithm:
1. Get the current head from the store
2. Calculate the appropriate attestation target using current forkchoice state
3. Use the store's latest justified checkpoint as the attestation source
4. Construct and return the complete Attestation object

Args:
store: The forkchoice store providing the current chain view.
slot: The slot for which to produce the attestation.

Returns:
A fully constructed Attestation object ready for signing and broadcast.
"""
# Get the head block the validator sees for this slot
head_checkpoint = Checkpoint(
root=store.head,
slot=store.blocks[store.head].slot,
)

# Calculate the target checkpoint for this attestation
target_checkpoint = store.get_attestation_target()

# Create the attestation using current forkchoice state
return Attestation(
validator_id=self.index,
data=AttestationData(
slot=slot,
head=head_checkpoint,
target=target_checkpoint,
source=store.latest_justified,
),
)
54 changes: 0 additions & 54 deletions src/lean_spec/subspecs/forkchoice/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
)
from lean_spec.subspecs.containers import (
Attestation,
AttestationData,
Block,
BlockBody,
Checkpoint,
Expand Down Expand Up @@ -934,56 +933,3 @@ def produce_block_with_signatures(
)

return store, finalized_block, signatures

def produce_attestation(
self,
slot: Slot,
validator_index: ValidatorIndex,
) -> Attestation:
"""
Produce an attestation for the given slot and validator.

This method constructs an Attestation object according to the lean protocol
specification for attestation. The attestation represents the
validator's view of the chain state and their choice for the
next justified checkpoint.

The algorithm:
1. Get the current head
2. Calculate the appropriate attestation target using current forkchoice state
3. Use the store's latest justified checkpoint as the attestation source
4. Construct and return the complete Attestation object

Args:
slot: The slot for which to produce the attestation.
validator_index: The validator index producing the attestation.

Returns:
A fully constructed Attestation object ready for signing and broadcast.
"""
# Get the head block the validator sees for this slot
head_checkpoint = Checkpoint(
root=self.head,
slot=self.blocks[self.head].slot,
)

# Calculate the target checkpoint for this attestation
#
# This uses the store's current forkchoice state to determine
# the appropriate attestation target, balancing between head
# advancement and safety guarantees.
target_checkpoint = self.get_attestation_target()

# Construct attestation data
attestation_data = AttestationData(
slot=slot,
head=head_checkpoint,
target=target_checkpoint,
source=self.latest_justified,
)

# Create the attestation using current forkchoice state
return Attestation(
validator_id=validator_index,
data=attestation_data,
)
35 changes: 23 additions & 12 deletions tests/lean_spec/subspecs/forkchoice/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,8 @@ def test_produce_attestation_basic(self, sample_store: Store) -> None:
slot = Slot(1)
validator_idx = ValidatorIndex(5)

attestation = sample_store.produce_attestation(slot, validator_idx)
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
attestation = validator.produce_attestation(sample_store, slot)

# Verify attestation structure
assert attestation.validator_id == validator_idx
Expand All @@ -313,7 +314,8 @@ def test_produce_attestation_head_reference(self, sample_store: Store) -> None:
slot = Slot(2)
validator_idx = ValidatorIndex(8)

attestation = sample_store.produce_attestation(slot, validator_idx)
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
attestation = validator.produce_attestation(sample_store, slot)

# Head checkpoint should reference the current proposal head
_, expected_head_root = sample_store.get_proposal_head(slot)
Expand All @@ -328,7 +330,8 @@ def test_produce_attestation_target_calculation(self, sample_store: Store) -> No
slot = Slot(3)
validator_idx = ValidatorIndex(9)

attestation = sample_store.produce_attestation(slot, validator_idx)
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
attestation = validator.produce_attestation(sample_store, slot)

# Target should match the store's attestation target calculation
expected_target = sample_store.get_attestation_target()
Expand All @@ -342,7 +345,8 @@ def test_produce_attestation_different_validators(self, sample_store: Store) ->
# All validators should produce consistent attestations for the same slot
attestations = []
for validator_idx in range(5):
attestation = sample_store.produce_attestation(slot, ValidatorIndex(validator_idx))
validator = Validator(pubkey=Bytes52.zero(), index=ValidatorIndex(validator_idx))
attestation = validator.produce_attestation(sample_store, slot)
attestations.append(attestation)

# Each attestation should have correct validator ID
Expand All @@ -362,10 +366,11 @@ def test_produce_attestation_different_validators(self, sample_store: Store) ->
def test_produce_attestation_sequential_slots(self, sample_store: Store) -> None:
"""Test attestation production across sequential slots."""
validator_idx = ValidatorIndex(3)
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)

# Produce attestations for sequential slots
attestation1 = sample_store.produce_attestation(Slot(1), validator_idx)
attestation2 = sample_store.produce_attestation(Slot(2), validator_idx)
attestation1 = validator.produce_attestation(sample_store, Slot(1))
attestation2 = validator.produce_attestation(sample_store, Slot(2))

# Attestations should be for different slots
assert attestation1.data.slot == Slot(1)
Expand All @@ -380,7 +385,8 @@ def test_produce_attestation_justification_consistency(self, sample_store: Store
slot = Slot(5)
validator_idx = ValidatorIndex(2)

attestation = sample_store.produce_attestation(slot, validator_idx)
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
attestation = validator.produce_attestation(sample_store, slot)

# Source must be the latest justified checkpoint from store
assert attestation.data.source.root == sample_store.latest_justified.root
Expand All @@ -406,7 +412,8 @@ 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)
attestation = sample_store.produce_attestation(attestor_slot, attestor_idx)
validator = Validator(pubkey=Bytes52.zero(), index=attestor_idx)
attestation = validator.produce_attestation(sample_store, attestor_slot)

# Attestation should reference the new block as head (if it became head)
assert attestation.validator_id == attestor_idx
Expand All @@ -428,7 +435,8 @@ def test_multiple_validators_coordination(self, sample_store: Store) -> None:
# These will be based on the current forkchoice head (genesis)
attestations = []
for i in range(2, 6):
attestation = sample_store.produce_attestation(Slot(2), ValidatorIndex(i))
validator = Validator(pubkey=Bytes52.zero(), index=ValidatorIndex(i))
attestation = validator.produce_attestation(sample_store, Slot(2))
attestations.append(attestation)

# All attestations should be consistent
Expand Down Expand Up @@ -482,7 +490,8 @@ def test_validator_edge_cases(self, sample_store: Store) -> None:
assert block.proposer_index == max_validator

# Should be able to produce attestation
attestation = sample_store.produce_attestation(Slot(10), max_validator)
validator = Validator(pubkey=Bytes52.zero(), index=max_validator)
attestation = validator.produce_attestation(sample_store, Slot(10))
assert attestation.validator_id == max_validator

def test_validator_operations_empty_store(self) -> None:
Expand Down Expand Up @@ -563,7 +572,8 @@ def test_validator_operations_empty_store(self) -> None:
Slot(1),
ValidatorIndex(1),
)
attestation = store.produce_attestation(Slot(1), ValidatorIndex(2))
validator = Validator(pubkey=Bytes52.zero(), index=ValidatorIndex(2))
attestation = validator.produce_attestation(store, Slot(1))

assert isinstance(block, Block)
assert isinstance(attestation, Attestation)
Expand Down Expand Up @@ -621,5 +631,6 @@ def test_validator_operations_invalid_parameters(self, sample_store: Store) -> N
assert isinstance(result, bool)

# produce_attestation should work for any validator
attestation = sample_store.produce_attestation(Slot(1), large_validator)
validator = Validator(pubkey=Bytes52.zero(), index=large_validator)
attestation = validator.produce_attestation(sample_store, Slot(1))
assert attestation.validator_id == large_validator
Loading