Skip to content

Commit bf46fa0

Browse files
tcoratgerclaude
andauthored
test: add mirror tests for Slot justification math (leanEthereum#891)
slot.py had no mirror test_slot.py; its non-trivial justification math was only exercised incidentally by fork-choice and state-transition tests. Add a dedicated mirror covering both methods. is_justifiable_after: pin the full distance table for deltas 0-30 (immediate window 0-5, pronic 6/12/20/30, perfect square 9/16/25, all others rejected), confirm justifiability tracks distance rather than absolute slot, allow the equal-slot boundary, and assert the full AssertionError message for a slot before finalization. justified_index_after: None at or before the finalized boundary, the first unfinalized slot mapped to index 0, and a one-per-slot offset across zero and non-zero finalized slots. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 88f057b commit bf46fa0

1 file changed

Lines changed: 120 additions & 0 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Tests for the Slot justification math."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from lean_spec.spec.forks.lstar.slot import Slot
8+
9+
10+
class TestIsJustifiableAfter:
11+
"""Distance rules deciding whether a slot may receive new justification votes."""
12+
13+
@pytest.mark.parametrize(
14+
("distance_from_finalized", "is_justifiable"),
15+
[
16+
(0, True), # immediate window
17+
(1, True),
18+
(2, True),
19+
(3, True),
20+
(4, True),
21+
(5, True), # last slot of the immediate window
22+
(6, True), # pronic 2*3
23+
(7, False),
24+
(8, False),
25+
(9, True), # perfect square 3**2
26+
(10, False),
27+
(11, False),
28+
(12, True), # pronic 3*4
29+
(13, False),
30+
(14, False),
31+
(15, False),
32+
(16, True), # perfect square 4**2
33+
(17, False),
34+
(18, False),
35+
(19, False),
36+
(20, True), # pronic 4*5
37+
(21, False),
38+
(22, False),
39+
(23, False),
40+
(24, False),
41+
(25, True), # perfect square 5**2
42+
(26, False),
43+
(27, False),
44+
(28, False),
45+
(29, False),
46+
(30, True), # pronic 5*6
47+
],
48+
)
49+
def test_follows_distance_pattern(
50+
self, distance_from_finalized: int, is_justifiable: bool
51+
) -> None:
52+
"""A slot is justifiable when its distance from finalization is small, square, or pronic."""
53+
finalized_slot = Slot(0)
54+
candidate_slot = Slot(distance_from_finalized)
55+
56+
assert candidate_slot.is_justifiable_after(finalized_slot) is is_justifiable
57+
58+
def test_depends_on_distance_not_absolute_slot(self) -> None:
59+
"""Justifiability is decided by the gap from finalization, not the absolute slot number."""
60+
finalized_slot = Slot(100)
61+
62+
assert Slot(106).is_justifiable_after(finalized_slot) is True # distance 6, pronic
63+
assert Slot(107).is_justifiable_after(finalized_slot) is False # distance 7
64+
assert Slot(109).is_justifiable_after(finalized_slot) is True # distance 9, perfect square
65+
66+
def test_allows_slot_equal_to_finalized(self) -> None:
67+
"""The finalized slot itself sits at distance zero and is justifiable."""
68+
finalized_slot = Slot(42)
69+
70+
assert finalized_slot.is_justifiable_after(finalized_slot) is True
71+
72+
def test_rejects_slot_before_finalized(self) -> None:
73+
"""A candidate earlier than the finalized slot is a programming error."""
74+
finalized_slot = Slot(5)
75+
76+
with pytest.raises(AssertionError) as exception_info:
77+
Slot(4).is_justifiable_after(finalized_slot)
78+
79+
assert str(exception_info.value) == "Candidate slot must not be before finalized slot"
80+
81+
82+
class TestJustifiedIndexAfter:
83+
"""Mapping a slot to its position in the relative justification bitfield."""
84+
85+
@pytest.mark.parametrize(
86+
("candidate_slot_value", "finalized_slot_value"),
87+
[
88+
(5, 5), # the finalized slot itself
89+
(3, 5), # a slot before finalization
90+
(0, 0), # genesis at the genesis boundary
91+
],
92+
)
93+
def test_returns_none_at_or_before_finalized(
94+
self, candidate_slot_value: int, finalized_slot_value: int
95+
) -> None:
96+
"""Slots at or before finalization have no index in the tracked bitfield."""
97+
candidate_slot = Slot(candidate_slot_value)
98+
finalized_slot = Slot(finalized_slot_value)
99+
100+
assert candidate_slot.justified_index_after(finalized_slot) is None
101+
102+
@pytest.mark.parametrize(
103+
("candidate_slot_value", "finalized_slot_value", "expected_index"),
104+
[
105+
(6, 5, 0), # first slot after finalization maps to index 0
106+
(7, 5, 1),
107+
(10, 5, 4),
108+
(1, 0, 0),
109+
(5, 0, 4),
110+
(13, 10, 2),
111+
],
112+
)
113+
def test_maps_slot_distance_to_zero_based_index(
114+
self, candidate_slot_value: int, finalized_slot_value: int, expected_index: int
115+
) -> None:
116+
"""The first slot after finalization is index 0, each later slot one higher."""
117+
candidate_slot = Slot(candidate_slot_value)
118+
finalized_slot = Slot(finalized_slot_value)
119+
120+
assert candidate_slot.justified_index_after(finalized_slot) == expected_index

0 commit comments

Comments
 (0)