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
5 changes: 3 additions & 2 deletions src/lean_spec/subspecs/forkchoice/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def validate_attestation(self, signed_vote: "SignedVote") -> None:
# Validate vote targets exist in store
assert vote.source.root in self.blocks, f"Unknown source block: {vote.source.root.hex()}"
assert vote.target.root in self.blocks, f"Unknown target block: {vote.target.root.hex()}"
assert vote.head.root in self.blocks, f"Unknown head block: {vote.head.root.hex()}"

# Validate slot relationships
source_block = self.blocks[vote.source.root]
Expand All @@ -136,7 +137,7 @@ def validate_attestation(self, signed_vote: "SignedVote") -> None:
assert target_block.slot == vote.target.slot, "Target checkpoint slot mismatch"

# Validate attestation is not too far in the future
current_slot = Slot(self.time // SECONDS_PER_INTERVAL)
current_slot = Slot(self.time // INTERVALS_PER_SLOT)
assert vote.slot <= Slot(current_slot + Slot(1)), "Attestation too far in future"

def process_attestation(self, signed_vote: "SignedVote", is_from_block: bool = False) -> None:
Expand Down Expand Up @@ -173,7 +174,7 @@ def process_attestation(self, signed_vote: "SignedVote", is_from_block: bool = F
# Network gossip attestation processing

# Ensure forkchoice is current before processing gossip
time_slots = self.time // SECONDS_PER_INTERVAL
time_slots = self.time // INTERVALS_PER_SLOT
assert vote.slot <= time_slots, "Attestation from future slot"

# Update new votes if this is latest from validator
Expand Down
46 changes: 46 additions & 0 deletions tests/lean_spec/subspecs/forkchoice/test_attestation_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,52 @@ def test_validate_attestation_too_far_future(self, sample_store: Store) -> None:
with pytest.raises(AssertionError, match="Attestation too far in future"):
sample_store.validate_attestation(signed_vote)

def test_validate_attestation_unknown_head_rejected(self, sample_store: Store) -> None:
"""Test validation fails when head block is unknown.

This ensures consistency with source/target validation and prevents
spam attestations with fabricated head roots.
"""
# Create valid source and target blocks
source_block = Block(
slot=Slot(1),
proposer_index=Uint64(1),
parent_root=Bytes32.zero(),
state_root=Bytes32(b"source" + b"\x00" * 26),
body=BlockBody(attestations=Attestations(data=[])),
)
source_hash = hash_tree_root(source_block)

target_block = Block(
slot=Slot(2),
proposer_index=Uint64(2),
parent_root=source_hash,
state_root=Bytes32(b"target" + b"\x00" * 26),
body=BlockBody(attestations=Attestations(data=[])),
)
target_hash = hash_tree_root(target_block)

# Add source and target blocks to store
sample_store.blocks[source_hash] = source_block
sample_store.blocks[target_hash] = target_block

# Create an unknown head root that doesn't exist in the store
unknown_head_root = Bytes32(b"\x99" * 32)

# Create vote with unknown head but valid source and target
vote = Vote(
validator_id=ValidatorIndex(0),
slot=Slot(2),
head=Checkpoint(root=unknown_head_root, slot=Slot(2)), # Unknown head!
target=Checkpoint(root=target_hash, slot=Slot(2)),
source=Checkpoint(root=source_hash, slot=Slot(1)),
)
signed_vote = SignedVote(data=vote, signature=Bytes32.zero())

# Should raise assertion error for unknown head
with pytest.raises(AssertionError, match="Unknown head block"):
sample_store.validate_attestation(signed_vote)


class TestAttestationProcessing:
"""Test attestation processing logic."""
Expand Down
Loading