1414class TestAggregationBitsToValidatorIndices :
1515 """Convert an aggregation bitlist to the validator indices it encodes."""
1616
17- def test_reject_empty_aggregation_bits (self ) -> None :
18- """No bits set must raise."""
17+ def test_reject_zero_length_bits (self ) -> None :
18+ """A zero-length bitlist raises with the exact empty-aggregation message."""
19+ bits = AggregationBits (data = [])
20+ with pytest .raises (AssertionError ) as exc_info :
21+ bits .to_validator_indices ()
22+ assert str (exc_info .value ) == "Aggregated attestation must reference at least one validator"
23+
24+ def test_reject_all_false_bits (self ) -> None :
25+ """A bitlist with no bits set raises with the exact empty-aggregation message."""
1926 bits = AggregationBits (data = [Boolean (False ), Boolean (False ), Boolean (False )])
20- with pytest .raises (
21- AssertionError ,
22- match = "Aggregated attestation must reference at least one validator" ,
23- ):
27+ with pytest .raises (AssertionError ) as exc_info :
2428 bits .to_validator_indices ()
29+ assert str (exc_info .value ) == "Aggregated attestation must reference at least one validator"
30+
31+ def test_single_bit_set_at_position_zero (self ) -> None :
32+ """Only bit 0 set yields the single index 0."""
33+ bits = AggregationBits (data = [Boolean (True )])
34+ assert bits .to_validator_indices () == ValidatorIndices (data = [ValidatorIndex (0 )])
2535
26- def test_single_bit_set (self ) -> None :
27- """One bit set yields a single-index list."""
36+ def test_single_bit_set_in_middle (self ) -> None :
37+ """One non-leading bit set yields a single-index list at that position ."""
2838 bits = AggregationBits (data = [Boolean (False ), Boolean (True ), Boolean (False )])
2939 assert bits .to_validator_indices () == ValidatorIndices (data = [ValidatorIndex (1 )])
3040
31- def test_multiple_bits_set (self ) -> None :
41+ def test_single_bit_set_at_last_valid_position (self ) -> None :
42+ """Bitlist of length LIMIT with only the final bit set yields index LIMIT - 1."""
43+ data = [Boolean (False )] * (int (VALIDATOR_REGISTRY_LIMIT ) - 1 ) + [Boolean (True )]
44+ bits = AggregationBits (data = data )
45+ assert bits .to_validator_indices () == ValidatorIndices (
46+ data = [ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ) - 1 )]
47+ )
48+
49+ def test_all_bits_set_at_full_limit (self ) -> None :
50+ """All LIMIT bits set yields every validator index from 0 to LIMIT - 1 inclusive."""
51+ bits = AggregationBits (data = [Boolean (True )] * int (VALIDATOR_REGISTRY_LIMIT ))
52+ expected = ValidatorIndices (
53+ data = [ValidatorIndex (i ) for i in range (int (VALIDATOR_REGISTRY_LIMIT ))]
54+ )
55+ assert bits .to_validator_indices () == expected
56+
57+ def test_multiple_bits_set_ascending (self ) -> None :
3258 """Multiple bits set yield indices in ascending order."""
3359 bits = AggregationBits (
3460 data = [Boolean (True ), Boolean (False ), Boolean (True ), Boolean (True ), Boolean (False )]
@@ -37,34 +63,88 @@ def test_multiple_bits_set(self) -> None:
3763 data = [ValidatorIndex (0 ), ValidatorIndex (2 ), ValidatorIndex (3 )]
3864 )
3965
66+ def test_two_bits_set_at_extremes (self ) -> None :
67+ """Bits at positions 0 and LIMIT - 1 produce the two extreme indices in order."""
68+ data = (
69+ [Boolean (True )]
70+ + [Boolean (False )] * (int (VALIDATOR_REGISTRY_LIMIT ) - 2 )
71+ + [Boolean (True )]
72+ )
73+ bits = AggregationBits (data = data )
74+ assert bits .to_validator_indices () == ValidatorIndices (
75+ data = [ValidatorIndex (0 ), ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ) - 1 )]
76+ )
77+
78+ def test_alternating_even_positions (self ) -> None :
79+ """Bits true at every even position from 0 to 10 yield exactly those indices."""
80+ data = [Boolean (i % 2 == 0 ) for i in range (11 )]
81+ bits = AggregationBits (data = data )
82+ assert bits .to_validator_indices () == ValidatorIndices (
83+ data = [ValidatorIndex (i ) for i in (0 , 2 , 4 , 6 , 8 , 10 )]
84+ )
85+
86+ def test_trailing_false_bits_are_ignored (self ) -> None :
87+ """Trailing false bits after the last set bit do not influence the index list."""
88+ data = [Boolean (False )] * 10
89+ data [2 ] = Boolean (True )
90+ bits = AggregationBits (data = data )
91+ assert bits .to_validator_indices () == ValidatorIndices (data = [ValidatorIndex (2 )])
92+
4093
4194class TestValidatorIndicesToAggregationBits :
4295 """Convert a list of validator indices to an aggregation bitlist."""
4396
4497 def test_reject_empty_indices (self ) -> None :
45- """An empty index list must raise ."""
98+ """An empty index list raises with the exact empty-aggregation message ."""
4699 indices = ValidatorIndices (data = [])
47- with pytest .raises (
48- AssertionError ,
49- match = "Aggregated attestation must reference at least one validator" ,
50- ):
100+ with pytest .raises (AssertionError ) as exc_info :
51101 indices .to_aggregation_bits ()
102+ assert str (exc_info .value ) == "Aggregated attestation must reference at least one validator"
103+
104+ def test_single_index_zero (self ) -> None :
105+ """Single index 0 produces a one-bit bitlist with the only bit set."""
106+ indices = ValidatorIndices (data = [ValidatorIndex (0 )])
107+ assert indices .to_aggregation_bits () == AggregationBits (data = [Boolean (True )])
108+
109+ def test_single_index_at_last_valid_position (self ) -> None :
110+ """Single index LIMIT - 1 produces a LIMIT-length bitlist with only the final bit true."""
111+ indices = ValidatorIndices (data = [ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ) - 1 )])
112+ expected_data = [Boolean (False )] * (int (VALIDATOR_REGISTRY_LIMIT ) - 1 ) + [Boolean (True )]
113+ assert indices .to_aggregation_bits () == AggregationBits (data = expected_data )
52114
53115 def test_reject_index_at_limit (self ) -> None :
54- """An index equal to VALIDATOR_REGISTRY_LIMIT exceeds the bitfield range."""
55- indices = ValidatorIndices (data = [ValidatorIndex (VALIDATOR_REGISTRY_LIMIT )])
56- with pytest .raises (
57- AssertionError , match = "Validator index out of range for aggregation bits"
58- ):
116+ """An index equal to LIMIT exceeds the bitfield range and raises exact message."""
117+ indices = ValidatorIndices (data = [ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ))])
118+ with pytest .raises (AssertionError ) as exc_info :
59119 indices .to_aggregation_bits ()
120+ assert str (exc_info .value ) == "Validator index out of range for aggregation bits"
60121
61- def test_reject_index_above_limit (self ) -> None :
62- """An index above VALIDATOR_REGISTRY_LIMIT exceeds the bitfield range."""
63- indices = ValidatorIndices (data = [ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ) + 100 )])
64- with pytest .raises (
65- AssertionError , match = "Validator index out of range for aggregation bits"
66- ):
122+ def test_reject_index_just_above_limit (self ) -> None :
123+ """An index of LIMIT + 1 raises the exact out-of-range message."""
124+ indices = ValidatorIndices (data = [ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ) + 1 )])
125+ with pytest .raises (AssertionError ) as exc_info :
67126 indices .to_aggregation_bits ()
127+ assert str (exc_info .value ) == "Validator index out of range for aggregation bits"
128+
129+ def test_reject_index_far_above_limit (self ) -> None :
130+ """An index well above LIMIT raises the exact out-of-range message."""
131+ indices = ValidatorIndices (data = [ValidatorIndex (2 ** 20 )])
132+ with pytest .raises (AssertionError ) as exc_info :
133+ indices .to_aggregation_bits ()
134+ assert str (exc_info .value ) == "Validator index out of range for aggregation bits"
135+
136+ def test_one_bad_index_poisons_the_batch (self ) -> None :
137+ """A mix of valid indices and one out-of-range index raises the out-of-range message."""
138+ indices = ValidatorIndices (
139+ data = [
140+ ValidatorIndex (0 ),
141+ ValidatorIndex (1 ),
142+ ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT )),
143+ ]
144+ )
145+ with pytest .raises (AssertionError ) as exc_info :
146+ indices .to_aggregation_bits ()
147+ assert str (exc_info .value ) == "Validator index out of range for aggregation bits"
68148
69149 def test_deduplicates_repeated_indices (self ) -> None :
70150 """Repeated indices collapse to a single set bit per position."""
@@ -75,6 +155,13 @@ def test_deduplicates_repeated_indices(self) -> None:
75155 data = [Boolean (False ), Boolean (True ), Boolean (False ), Boolean (True )]
76156 )
77157
158+ def test_all_duplicate_input (self ) -> None :
159+ """An input of repeated index 5 yields a length-6 bitlist with only the final bit true."""
160+ indices = ValidatorIndices (data = [ValidatorIndex (5 )] * 4 )
161+ assert indices .to_aggregation_bits () == AggregationBits (
162+ data = [Boolean (False )] * 5 + [Boolean (True )]
163+ )
164+
78165 def test_handles_unsorted_input (self ) -> None :
79166 """Unsorted indices produce a positionally correct bitfield."""
80167 indices = ValidatorIndices (data = [ValidatorIndex (5 ), ValidatorIndex (1 ), ValidatorIndex (3 )])
@@ -89,14 +176,90 @@ def test_handles_unsorted_input(self) -> None:
89176 ]
90177 )
91178
179+ def test_unsorted_with_duplicates (self ) -> None :
180+ """Unsorted input with duplicates yields a length-6 bitlist set at 1, 3, and 5."""
181+ indices = ValidatorIndices (
182+ data = [
183+ ValidatorIndex (5 ),
184+ ValidatorIndex (1 ),
185+ ValidatorIndex (3 ),
186+ ValidatorIndex (5 ),
187+ ValidatorIndex (1 ),
188+ ]
189+ )
190+ assert indices .to_aggregation_bits () == AggregationBits (
191+ data = [
192+ Boolean (False ),
193+ Boolean (True ),
194+ Boolean (False ),
195+ Boolean (True ),
196+ Boolean (False ),
197+ Boolean (True ),
198+ ]
199+ )
200+
92201 def test_length_matches_max_index (self ) -> None :
93- """Returned bitfield has length max_index + 1 — no trailing zeros."""
202+ """Returned bitfield has length max_index + 1 with no trailing zeros."""
94203 indices = ValidatorIndices (data = [ValidatorIndex (3 )])
95204 assert indices .to_aggregation_bits () == AggregationBits (
96205 data = [Boolean (False ), Boolean (False ), Boolean (False ), Boolean (True )]
97206 )
98207
99- def test_roundtrip_through_aggregation_bits (self ) -> None :
208+ def test_dense_full_registry (self ) -> None :
209+ """Every index from 0 to LIMIT - 1 yields a LIMIT-length all-true bitlist."""
210+ indices = ValidatorIndices (
211+ data = [ValidatorIndex (i ) for i in range (int (VALIDATOR_REGISTRY_LIMIT ))]
212+ )
213+ assert indices .to_aggregation_bits () == AggregationBits (
214+ data = [Boolean (True )] * int (VALIDATOR_REGISTRY_LIMIT )
215+ )
216+
217+ def test_sparse_maximum (self ) -> None :
218+ """Indices 0 and LIMIT - 1 yield a LIMIT-length bitlist with only those two bits set."""
219+ indices = ValidatorIndices (
220+ data = [ValidatorIndex (0 ), ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ) - 1 )]
221+ )
222+ expected_data = (
223+ [Boolean (True )]
224+ + [Boolean (False )] * (int (VALIDATOR_REGISTRY_LIMIT ) - 2 )
225+ + [Boolean (True )]
226+ )
227+ assert indices .to_aggregation_bits () == AggregationBits (data = expected_data )
228+
229+
230+ class TestRoundTrip :
231+ """Round-trip symmetry between AggregationBits and ValidatorIndices."""
232+
233+ def test_roundtrip_indices_to_bits_to_indices (self ) -> None :
100234 """Indices to bits and back yields the original input."""
101235 original = ValidatorIndices (data = [ValidatorIndex (1 ), ValidatorIndex (5 ), ValidatorIndex (7 )])
102236 assert original .to_aggregation_bits ().to_validator_indices () == original
237+
238+ def test_roundtrip_bits_to_indices_to_bits_preserves_last_true (self ) -> None :
239+ """Bits to indices and back is exact when the bitlist ends with a true bit."""
240+ original = AggregationBits (
241+ data = [
242+ Boolean (False ),
243+ Boolean (True ),
244+ Boolean (False ),
245+ Boolean (True ),
246+ Boolean (True ),
247+ Boolean (False ),
248+ Boolean (False ),
249+ Boolean (True ),
250+ ]
251+ )
252+ assert original .to_validator_indices ().to_aggregation_bits () == original
253+
254+ def test_roundtrip_bits_to_indices_to_bits_trims_trailing_false (self ) -> None :
255+ """Round-trip trims trailing false bits because output is sized by max index plus one."""
256+ original = AggregationBits (
257+ data = [Boolean (False ), Boolean (True ), Boolean (False ), Boolean (False )]
258+ )
259+ trimmed = AggregationBits (data = [Boolean (False ), Boolean (True )])
260+ assert original .to_validator_indices ().to_aggregation_bits () == trimmed
261+
262+ def test_roundtrip_boundary_index (self ) -> None :
263+ """Round-trip starting from the highest valid index preserves the input exactly."""
264+ original = ValidatorIndices (data = [ValidatorIndex (int (VALIDATOR_REGISTRY_LIMIT ) - 1 )])
265+ assert original .to_aggregation_bits ().to_validator_indices () == original
0 commit comments