Skip to content

Commit ce53141

Browse files
tcoratgerclaude
andauthored
refactor(testing): share one selective-check base across the check models (leanEthereum#876)
StoreChecks drove its scalar, label-root, and pool target-slot checks through three parallel if-ladders, and AttestationCheck repeated a fourth as a field-name chain. StateExpectation already had the clean accessor-table form. Extract a SelectiveCheck base that iterates only the fields a test set against a subclass accessor table. StoreChecks and StateExpectation inherit it; each repetitive group collapses to one table-driven loop, and AttestationCheck reads through a module-level slot accessor table. Bespoke checks that are not one-line lookups (lexicographic head, reorg depth, block-body structure) stay explicit. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 39b94e6 commit ce53141

3 files changed

Lines changed: 96 additions & 120 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Shared base for expectation models that validate only their set fields."""
2+
3+
from collections.abc import Callable
4+
from typing import Any, ClassVar
5+
6+
from lean_spec.base import CamelModel
7+
8+
9+
class SelectiveCheck(CamelModel):
10+
"""Validates only the fields a test explicitly set, via a subclass accessor table."""
11+
12+
_SCALAR_ACCESSORS: ClassVar[dict[str, Callable[[Any], Any]]] = {}
13+
"""Field name to reader over the validated target."""
14+
15+
def validate_scalar_fields(self, target: Any, failure_prefix: str) -> None:
16+
"""
17+
Check every explicitly-set scalar field against the target.
18+
19+
Args:
20+
target: Object the accessors read the actual values from.
21+
failure_prefix: Prefix for the assertion message on mismatch.
22+
23+
Raises:
24+
AssertionError: When a set field disagrees with the target.
25+
"""
26+
for field_name in self.model_fields_set & self._SCALAR_ACCESSORS.keys():
27+
expected_value = getattr(self, field_name)
28+
actual_value = self._SCALAR_ACCESSORS[field_name](target)
29+
if actual_value != expected_value:
30+
raise AssertionError(
31+
f"{failure_prefix}: {field_name} = {actual_value}, expected {expected_value}"
32+
)

packages/testing/src/consensus_testing/test_types/state_expectation.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from collections.abc import Callable
44
from typing import Any, ClassVar
55

6+
from consensus_testing.test_types.selective_check import SelectiveCheck
67
from consensus_testing.test_types.utils import resolve_block_root
7-
from lean_spec.base import CamelModel
88
from lean_spec.spec.forks import Slot
99
from lean_spec.spec.forks.lstar.containers import (
1010
Block,
@@ -17,7 +17,7 @@
1717
from lean_spec.spec.ssz import Bytes32
1818

1919

20-
class StateExpectation(CamelModel):
20+
class StateExpectation(SelectiveCheck):
2121
"""
2222
Expected State fields after state transition (selective validation).
2323
@@ -35,7 +35,7 @@ class StateExpectation(CamelModel):
3535
)
3636
"""
3737

38-
_ACCESSORS: ClassVar[dict[str, Callable[["State"], Any]]] = {
38+
_SCALAR_ACCESSORS: ClassVar[dict[str, Callable[["State"], Any]]] = {
3939
"slot": lambda s: s.slot,
4040
"latest_justified_slot": lambda s: s.latest_justified.slot,
4141
"latest_justified_root": lambda s: s.latest_justified.root,
@@ -164,15 +164,7 @@ def _resolve(label: str) -> Bytes32:
164164
raise ValueError(f"label '{label}' specified but block_registry not provided")
165165
return resolve_block_root(label, block_registry)
166166

167-
for field_name in fields & self._ACCESSORS.keys():
168-
accessor = self._ACCESSORS[field_name]
169-
expected_field_value = getattr(self, field_name)
170-
actual_field_value = accessor(state)
171-
if actual_field_value != expected_field_value:
172-
raise AssertionError(
173-
f"State validation failed: {field_name} = {actual_field_value}, "
174-
f"expected {expected_field_value}"
175-
)
167+
self.validate_scalar_fields(state, "State validation failed")
176168

177169
if "latest_justified_root_label" in fields:
178170
assert self.latest_justified_root_label is not None

packages/testing/src/consensus_testing/test_types/store_checks.py

Lines changed: 60 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Store checks model for selective validation in fork choice tests."""
22

3-
from typing import Literal
3+
from collections.abc import Callable
4+
from typing import Any, ClassVar, Literal
45

6+
from consensus_testing.test_types.selective_check import SelectiveCheck
57
from consensus_testing.test_types.utils import resolve_block_root
68
from lean_spec.base import CamelModel
79
from lean_spec.spec.crypto.merkleization import hash_tree_root
@@ -10,6 +12,14 @@
1012
from lean_spec.spec.forks.lstar.spec import LstarSpec
1113
from lean_spec.spec.ssz import ZERO_HASH, Bytes32
1214

15+
_ATTESTATION_SLOT_ACCESSORS: dict[str, Callable[[AttestationData], Slot]] = {
16+
"attestation_slot": lambda attestation: attestation.slot,
17+
"head_slot": lambda attestation: attestation.head.slot,
18+
"source_slot": lambda attestation: attestation.source.slot,
19+
"target_slot": lambda attestation: attestation.target.slot,
20+
}
21+
"""Per-validator attestation check field to the slot it reads."""
22+
1323

1424
def _ancestor_set(blocks: dict[Bytes32, Block], head: Bytes32) -> set[Bytes32]:
1525
"""Walk parent links from head and collect every reachable block root."""
@@ -76,52 +86,54 @@ def validate_attestation(
7686
self, attestation: "AttestationData", location: str, step_index: int
7787
) -> None:
7888
"""Validate attestation properties."""
79-
fields_to_check = self.model_fields_set - {"validator", "location"}
80-
81-
for field_name in fields_to_check:
89+
for field_name in self.model_fields_set & _ATTESTATION_SLOT_ACCESSORS.keys():
8290
expected_slot = getattr(self, field_name)
83-
84-
if field_name == "attestation_slot":
85-
actual_slot = attestation.slot
86-
if actual_slot != expected_slot:
87-
raise AssertionError(
88-
f"Step {step_index}: validator {self.validator} {location} "
89-
f"attestation slot = {actual_slot}, expected {expected_slot}"
90-
)
91-
92-
elif field_name == "head_slot":
93-
actual_slot = attestation.head.slot
94-
if actual_slot != expected_slot:
95-
raise AssertionError(
96-
f"Step {step_index}: validator {self.validator} {location} "
97-
f"head slot = {actual_slot}, expected {expected_slot}"
98-
)
99-
100-
elif field_name == "source_slot":
101-
actual_slot = attestation.source.slot
102-
if actual_slot != expected_slot:
103-
raise AssertionError(
104-
f"Step {step_index}: validator {self.validator} {location} "
105-
f"source slot = {actual_slot}, expected {expected_slot}"
106-
)
107-
108-
elif field_name == "target_slot":
109-
actual_slot = attestation.target.slot
110-
if actual_slot != expected_slot:
111-
raise AssertionError(
112-
f"Step {step_index}: validator {self.validator} {location} "
113-
f"target slot = {actual_slot}, expected {expected_slot}"
114-
)
91+
actual_slot = _ATTESTATION_SLOT_ACCESSORS[field_name](attestation)
92+
if actual_slot != expected_slot:
93+
raise AssertionError(
94+
f"Step {step_index}: validator {self.validator} {location} "
95+
f"{field_name} = {actual_slot}, expected {expected_slot}"
96+
)
11597

11698

117-
class StoreChecks(CamelModel):
99+
class StoreChecks(SelectiveCheck):
118100
"""
119101
Store state checks for fork choice tests.
120102
121103
All fields are optional. Only specified fields are validated.
122104
This allows tests to focus on the properties they care about.
123105
"""
124106

107+
_SCALAR_ACCESSORS: ClassVar[dict[str, Callable[[Store], Any]]] = {
108+
"time": lambda store: store.time,
109+
"head_slot": lambda store: store.blocks[store.head].slot,
110+
"head_root": lambda store: store.head,
111+
"latest_justified_slot": lambda store: store.latest_justified.slot,
112+
"latest_justified_root": lambda store: store.latest_justified.root,
113+
"latest_finalized_slot": lambda store: store.latest_finalized.slot,
114+
"latest_finalized_root": lambda store: store.latest_finalized.root,
115+
"safe_target": lambda store: store.safe_target,
116+
"safe_target_slot": lambda store: store.blocks[store.safe_target].slot,
117+
}
118+
"""Scalar field to the store value it must equal."""
119+
120+
_LABEL_ROOT_ACCESSORS: ClassVar[dict[str, Callable[[Store], Bytes32]]] = {
121+
"head_root_label": lambda store: store.head,
122+
"latest_justified_root_label": lambda store: store.latest_justified.root,
123+
"latest_finalized_root_label": lambda store: store.latest_finalized.root,
124+
"safe_target_root_label": lambda store: store.safe_target,
125+
}
126+
"""Label-reference field to the store root it must resolve to."""
127+
128+
_POOL_TARGET_SLOT_ACCESSORS: ClassVar[dict[str, Callable[[Store], Any]]] = {
129+
"attestation_signature_target_slots": lambda store: store.attestation_signatures,
130+
"latest_new_aggregated_target_slots": lambda store: store.latest_new_aggregated_payloads,
131+
"latest_known_aggregated_target_slots": (
132+
lambda store: store.latest_known_aggregated_payloads
133+
),
134+
}
135+
"""Pool target-slot field to the pool whose keyed target slots it compares."""
136+
125137
time: Interval | None = None
126138
"""Expected store time (in intervals since genesis)."""
127139

@@ -316,30 +328,13 @@ def _resolve(label: str) -> Bytes32:
316328
return resolve_block_root(label, block_registry)
317329

318330
# Scalar store fields
319-
if "time" in fields:
320-
_check("time", store.time, self.time)
321-
if "head_slot" in fields:
322-
_check("head.slot", store.blocks[store.head].slot, self.head_slot)
323-
if "head_root" in fields:
324-
_check("head.root", store.head, self.head_root)
325-
if "latest_justified_slot" in fields:
326-
_check("latest_justified.slot", store.latest_justified.slot, self.latest_justified_slot)
327-
if "latest_justified_root" in fields:
328-
_check("latest_justified.root", store.latest_justified.root, self.latest_justified_root)
329-
if "latest_finalized_slot" in fields:
330-
_check("latest_finalized.slot", store.latest_finalized.slot, self.latest_finalized_slot)
331-
if "latest_finalized_root" in fields:
332-
_check("latest_finalized.root", store.latest_finalized.root, self.latest_finalized_root)
333-
if "safe_target" in fields:
334-
_check("safe_target", store.safe_target, self.safe_target)
335-
if "safe_target_slot" in fields:
336-
_check("safe_target.slot", store.blocks[store.safe_target].slot, self.safe_target_slot)
331+
self.validate_scalar_fields(store, f"Step {step_index}")
337332

338333
# Label-based root checks (resolve label -> root, then compare)
339-
if "head_root_label" in fields:
340-
assert self.head_root_label is not None
341-
expected_head_root = _resolve(self.head_root_label)
342-
_check("head.root", store.head, expected_head_root)
334+
for field_name in fields & self._LABEL_ROOT_ACCESSORS.keys():
335+
expected_root = _resolve(getattr(self, field_name))
336+
_check(field_name, self._LABEL_ROOT_ACCESSORS[field_name](store), expected_root)
337+
343338
if "filled_block_root_label" in fields:
344339
if filled_block is None:
345340
raise ValueError(
@@ -349,18 +344,6 @@ def _resolve(label: str) -> Bytes32:
349344
assert self.filled_block_root_label is not None
350345
expected_filled_block_root = _resolve(self.filled_block_root_label)
351346
_check("filled_block.root", hash_tree_root(filled_block), expected_filled_block_root)
352-
if "latest_justified_root_label" in fields:
353-
assert self.latest_justified_root_label is not None
354-
expected_justified_root = _resolve(self.latest_justified_root_label)
355-
_check("latest_justified.root", store.latest_justified.root, expected_justified_root)
356-
if "latest_finalized_root_label" in fields:
357-
assert self.latest_finalized_root_label is not None
358-
expected_finalized_root = _resolve(self.latest_finalized_root_label)
359-
_check("latest_finalized.root", store.latest_finalized.root, expected_finalized_root)
360-
if "safe_target_root_label" in fields:
361-
assert self.safe_target_root_label is not None
362-
expected_safe_target_root = _resolve(self.safe_target_root_label)
363-
_check("safe_target", store.safe_target, expected_safe_target_root)
364347

365348
# Attestation target checkpoint (slot + root consistency)
366349
if "attestation_target_slot" in fields:
@@ -407,45 +390,14 @@ def _resolve(label: str) -> Bytes32:
407390
extracted_attestations[attestation_check.validator], label, step_index
408391
)
409392

410-
if "attestation_signature_target_slots" in fields:
411-
assert self.attestation_signature_target_slots is not None
412-
actual_target_slots = sorted(
413-
{attestation_data.target.slot for attestation_data in store.attestation_signatures}
414-
)
415-
expected_target_slots = sorted(self.attestation_signature_target_slots)
416-
_check(
417-
"attestation_signatures.target_slots", actual_target_slots, expected_target_slots
418-
)
419-
420-
if "latest_new_aggregated_target_slots" in fields:
421-
assert self.latest_new_aggregated_target_slots is not None
393+
# Target slots keyed in each attestation pool
394+
for field_name in fields & self._POOL_TARGET_SLOT_ACCESSORS.keys():
395+
pool = self._POOL_TARGET_SLOT_ACCESSORS[field_name](store)
422396
actual_target_slots = sorted(
423-
{
424-
attestation_data.target.slot
425-
for attestation_data in store.latest_new_aggregated_payloads
426-
}
427-
)
428-
expected_target_slots = sorted(self.latest_new_aggregated_target_slots)
429-
_check(
430-
"latest_new_aggregated_payloads.target_slots",
431-
actual_target_slots,
432-
expected_target_slots,
433-
)
434-
435-
if "latest_known_aggregated_target_slots" in fields:
436-
assert self.latest_known_aggregated_target_slots is not None
437-
actual_target_slots = sorted(
438-
{
439-
attestation_data.target.slot
440-
for attestation_data in store.latest_known_aggregated_payloads
441-
}
442-
)
443-
expected_target_slots = sorted(self.latest_known_aggregated_target_slots)
444-
_check(
445-
"latest_known_aggregated_payloads.target_slots",
446-
actual_target_slots,
447-
expected_target_slots,
397+
{attestation_data.target.slot for attestation_data in pool}
448398
)
399+
expected_target_slots = sorted(getattr(self, field_name))
400+
_check(field_name, actual_target_slots, expected_target_slots)
449401

450402
# Block body attestation count
451403
if "block_attestation_count" in fields:

0 commit comments

Comments
 (0)