Skip to content

Commit 22f2e44

Browse files
authored
containers: mv produce_attestation to Validator (#188)
* containers: mv produce_attestation to Validator * cleanup * fix comment * rm useless backup file
1 parent 1ec4866 commit 22f2e44

3 files changed

Lines changed: 98 additions & 67 deletions

File tree

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
"""Validator container for the Lean Ethereum consensus specification."""
22

3-
from lean_spec.types import Bytes52, Container
3+
from __future__ import annotations
4+
5+
from lean_spec.types import Bytes52, Container, ValidatorIndex
46

57
from ..xmss.containers import PublicKey
68
from ..xmss.interface import TEST_SIGNATURE_SCHEME, GeneralizedXmssScheme
9+
from .attestation import Attestation, AttestationData
710

811

912
class Validator(Container):
10-
"""Represents a validator's static metadata."""
13+
"""Represents a validator's static metadata and operational interface."""
1114

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

18+
index: ValidatorIndex = ValidatorIndex(0)
19+
"""Validator index in the registry."""
20+
1521
def get_pubkey(self, scheme: GeneralizedXmssScheme = TEST_SIGNATURE_SCHEME) -> PublicKey:
1622
"""Get the XMSS public key from this validator."""
1723
return PublicKey.from_bytes(bytes(self.pubkey), scheme.config)
24+
25+
def produce_attestation(self, data: AttestationData) -> Attestation:
26+
"""
27+
Produce an attestation from attestation data.
28+
29+
This method wraps AttestationData with the validator's identity to create
30+
a complete Attestation object ready for signing and broadcast.
31+
32+
Args:
33+
data: The attestation data containing slot, head, target, and source.
34+
35+
Returns:
36+
A fully constructed Attestation object with this validator's index.
37+
"""
38+
return Attestation(
39+
validator_id=self.index,
40+
data=data,
41+
)

src/lean_spec/subspecs/forkchoice/store.py

Lines changed: 37 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,43 @@ def get_attestation_target(self) -> Checkpoint:
867867
target_block = self.blocks[target_block_root]
868868
return Checkpoint(root=hash_tree_root(target_block), slot=target_block.slot)
869869

870+
def produce_attestation_data(self, slot: Slot) -> AttestationData:
871+
"""
872+
Produce attestation data for the given slot.
873+
874+
This method constructs an AttestationData object according to the lean protocol
875+
specification. The attestation data represents the chain state view including
876+
head, target, and source checkpoints.
877+
878+
The algorithm:
879+
1. Get the current head block
880+
2. Calculate the appropriate attestation target using current forkchoice state
881+
3. Use the store's latest justified checkpoint as the attestation source
882+
4. Construct and return the complete AttestationData object
883+
884+
Args:
885+
slot: The slot for which to produce the attestation data.
886+
887+
Returns:
888+
A fully constructed AttestationData object.
889+
"""
890+
# Get the head block the validator sees for this slot
891+
head_checkpoint = Checkpoint(
892+
root=self.head,
893+
slot=self.blocks[self.head].slot,
894+
)
895+
896+
# Calculate the target checkpoint for this attestation
897+
target_checkpoint = self.get_attestation_target()
898+
899+
# Construct attestation data
900+
return AttestationData(
901+
slot=slot,
902+
head=head_checkpoint,
903+
target=target_checkpoint,
904+
source=self.latest_justified,
905+
)
906+
870907
def produce_block_with_signatures(
871908
self,
872909
slot: Slot,
@@ -996,56 +1033,3 @@ def produce_block_with_signatures(
9961033
)
9971034

9981035
return store, finalized_block, signatures
999-
1000-
def produce_attestation(
1001-
self,
1002-
slot: Slot,
1003-
validator_index: ValidatorIndex,
1004-
) -> Attestation:
1005-
"""
1006-
Produce an attestation for the given slot and validator.
1007-
1008-
This method constructs an Attestation object according to the lean protocol
1009-
specification for attestation. The attestation represents the
1010-
validator's view of the chain state and their choice for the
1011-
next justified checkpoint.
1012-
1013-
The algorithm:
1014-
1. Get the current head
1015-
2. Calculate the appropriate attestation target using current forkchoice state
1016-
3. Use the store's latest justified checkpoint as the attestation source
1017-
4. Construct and return the complete Attestation object
1018-
1019-
Args:
1020-
slot: The slot for which to produce the attestation.
1021-
validator_index: The validator index producing the attestation.
1022-
1023-
Returns:
1024-
A fully constructed Attestation object ready for signing and broadcast.
1025-
"""
1026-
# Get the head block the validator sees for this slot
1027-
head_checkpoint = Checkpoint(
1028-
root=self.head,
1029-
slot=self.blocks[self.head].slot,
1030-
)
1031-
1032-
# Calculate the target checkpoint for this attestation
1033-
#
1034-
# This uses the store's current forkchoice state to determine
1035-
# the appropriate attestation target, balancing between head
1036-
# advancement and safety guarantees.
1037-
target_checkpoint = self.get_attestation_target()
1038-
1039-
# Construct attestation data
1040-
attestation_data = AttestationData(
1041-
slot=slot,
1042-
head=head_checkpoint,
1043-
target=target_checkpoint,
1044-
source=self.latest_justified,
1045-
)
1046-
1047-
# Create the attestation using current forkchoice state
1048-
return Attestation(
1049-
validator_id=validator_index,
1050-
data=attestation_data,
1051-
)

tests/lean_spec/subspecs/forkchoice/test_validator.py

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,9 @@ def test_produce_attestation_basic(self, sample_store: Store) -> None:
296296
slot = Slot(1)
297297
validator_idx = ValidatorIndex(5)
298298

299-
attestation = sample_store.produce_attestation(slot, validator_idx)
299+
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
300+
attestation_data = sample_store.produce_attestation_data(slot)
301+
attestation = validator.produce_attestation(attestation_data)
300302

301303
# Verify attestation structure
302304
assert attestation.validator_id == validator_idx
@@ -313,7 +315,9 @@ def test_produce_attestation_head_reference(self, sample_store: Store) -> None:
313315
slot = Slot(2)
314316
validator_idx = ValidatorIndex(8)
315317

316-
attestation = sample_store.produce_attestation(slot, validator_idx)
318+
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
319+
attestation_data = sample_store.produce_attestation_data(slot)
320+
attestation = validator.produce_attestation(attestation_data)
317321

318322
# Head checkpoint should reference the current proposal head
319323
_, expected_head_root = sample_store.get_proposal_head(slot)
@@ -328,7 +332,9 @@ def test_produce_attestation_target_calculation(self, sample_store: Store) -> No
328332
slot = Slot(3)
329333
validator_idx = ValidatorIndex(9)
330334

331-
attestation = sample_store.produce_attestation(slot, validator_idx)
335+
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
336+
attestation_data = sample_store.produce_attestation_data(slot)
337+
attestation = validator.produce_attestation(attestation_data)
332338

333339
# Target should match the store's attestation target calculation
334340
expected_target = sample_store.get_attestation_target()
@@ -342,7 +348,9 @@ def test_produce_attestation_different_validators(self, sample_store: Store) ->
342348
# All validators should produce consistent attestations for the same slot
343349
attestations = []
344350
for validator_idx in range(5):
345-
attestation = sample_store.produce_attestation(slot, ValidatorIndex(validator_idx))
351+
validator = Validator(pubkey=Bytes52.zero(), index=ValidatorIndex(validator_idx))
352+
attestation_data = sample_store.produce_attestation_data(slot)
353+
attestation = validator.produce_attestation(attestation_data)
346354
attestations.append(attestation)
347355

348356
# Each attestation should have correct validator ID
@@ -362,10 +370,13 @@ def test_produce_attestation_different_validators(self, sample_store: Store) ->
362370
def test_produce_attestation_sequential_slots(self, sample_store: Store) -> None:
363371
"""Test attestation production across sequential slots."""
364372
validator_idx = ValidatorIndex(3)
373+
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
365374

366375
# Produce attestations for sequential slots
367-
attestation1 = sample_store.produce_attestation(Slot(1), validator_idx)
368-
attestation2 = sample_store.produce_attestation(Slot(2), validator_idx)
376+
attestation_data1 = sample_store.produce_attestation_data(Slot(1))
377+
attestation1 = validator.produce_attestation(attestation_data1)
378+
attestation_data2 = sample_store.produce_attestation_data(Slot(2))
379+
attestation2 = validator.produce_attestation(attestation_data2)
369380

370381
# Attestations should be for different slots
371382
assert attestation1.data.slot == Slot(1)
@@ -380,7 +391,9 @@ def test_produce_attestation_justification_consistency(self, sample_store: Store
380391
slot = Slot(5)
381392
validator_idx = ValidatorIndex(2)
382393

383-
attestation = sample_store.produce_attestation(slot, validator_idx)
394+
validator = Validator(pubkey=Bytes52.zero(), index=validator_idx)
395+
attestation_data = sample_store.produce_attestation_data(slot)
396+
attestation = validator.produce_attestation(attestation_data)
384397

385398
# Source must be the latest justified checkpoint from store
386399
assert attestation.data.source.root == sample_store.latest_justified.root
@@ -406,7 +419,9 @@ def test_block_production_then_attestation(self, sample_store: Store) -> None:
406419
# Other validator creates attestation for slot 2
407420
attestor_slot = Slot(2)
408421
attestor_idx = ValidatorIndex(7)
409-
attestation = sample_store.produce_attestation(attestor_slot, attestor_idx)
422+
validator = Validator(pubkey=Bytes52.zero(), index=attestor_idx)
423+
attestation_data = sample_store.produce_attestation_data(attestor_slot)
424+
attestation = validator.produce_attestation(attestation_data)
410425

411426
# Attestation should reference the new block as head (if it became head)
412427
assert attestation.validator_id == attestor_idx
@@ -428,7 +443,9 @@ def test_multiple_validators_coordination(self, sample_store: Store) -> None:
428443
# These will be based on the current forkchoice head (genesis)
429444
attestations = []
430445
for i in range(2, 6):
431-
attestation = sample_store.produce_attestation(Slot(2), ValidatorIndex(i))
446+
validator = Validator(pubkey=Bytes52.zero(), index=ValidatorIndex(i))
447+
attestation_data = sample_store.produce_attestation_data(Slot(2))
448+
attestation = validator.produce_attestation(attestation_data)
432449
attestations.append(attestation)
433450

434451
# All attestations should be consistent
@@ -482,7 +499,9 @@ def test_validator_edge_cases(self, sample_store: Store) -> None:
482499
assert block.proposer_index == max_validator
483500

484501
# Should be able to produce attestation
485-
attestation = sample_store.produce_attestation(Slot(10), max_validator)
502+
validator = Validator(pubkey=Bytes52.zero(), index=max_validator)
503+
attestation_data = sample_store.produce_attestation_data(Slot(10))
504+
attestation = validator.produce_attestation(attestation_data)
486505
assert attestation.validator_id == max_validator
487506

488507
def test_validator_operations_empty_store(self) -> None:
@@ -563,7 +582,9 @@ def test_validator_operations_empty_store(self) -> None:
563582
Slot(1),
564583
ValidatorIndex(1),
565584
)
566-
attestation = store.produce_attestation(Slot(1), ValidatorIndex(2))
585+
validator = Validator(pubkey=Bytes52.zero(), index=ValidatorIndex(2))
586+
attestation_data = store.produce_attestation_data(Slot(1))
587+
attestation = validator.produce_attestation(attestation_data)
567588

568589
assert isinstance(block, Block)
569590
assert isinstance(attestation, Attestation)
@@ -621,5 +642,7 @@ def test_validator_operations_invalid_parameters(self, sample_store: Store) -> N
621642
assert isinstance(result, bool)
622643

623644
# produce_attestation should work for any validator
624-
attestation = sample_store.produce_attestation(Slot(1), large_validator)
645+
validator = Validator(pubkey=Bytes52.zero(), index=large_validator)
646+
attestation_data = sample_store.produce_attestation_data(Slot(1))
647+
attestation = validator.produce_attestation(attestation_data)
625648
assert attestation.validator_id == large_validator

0 commit comments

Comments
 (0)