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
57from consensus_testing .test_types .utils import resolve_block_root
68from lean_spec .base import CamelModel
79from lean_spec .spec .crypto .merkleization import hash_tree_root
1012from lean_spec .spec .forks .lstar .spec import LstarSpec
1113from 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
1424def _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