Skip to content

Commit 8b57b41

Browse files
tcoratgerclaude
andauthored
fix(testing): forbid non-integral float timestamps in slot-clock vectors (leanEthereum#1055)
The slot-clock operation union serializes its wall-clock timing inputs verbatim into the fixture JSON. The fields were typed float, so a fractional, non-exactly-representable timestamp could be authored and emitted. Such a value is a portability hazard: its decimal text can round-trip to a different binary value across JSON parsers and languages, breaking cross-client determinism. Add a Pydantic after-validator that rejects any timing input whose fractional part is non-zero (or is non-finite). Every authored value is a whole second or whole millisecond, so this changes no emitted vector; it only forbids future authors from introducing a fractional timestamp. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 419d5bb commit 8b57b41

1 file changed

Lines changed: 29 additions & 9 deletions

File tree

  • packages/testing/src/consensus_testing/test_fixtures

packages/testing/src/consensus_testing/test_fixtures/slot_clock.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Slot clock test fixture for timing conformance testing."""
22

3+
import math
34
from typing import Annotated, ClassVar, Literal
45

5-
from pydantic import Field
6+
from pydantic import AfterValidator, Field
67

78
from consensus_testing.test_fixtures.base import BaseConsensusFixture, BaseTestSpec
89
from lean_spec.base import StrictBaseModel
@@ -16,6 +17,25 @@
1617
from lean_spec.spec.ssz import Uint64
1718

1819

20+
def _reject_non_integral_timestamp(timestamp: float) -> float:
21+
"""
22+
Reject a fractional timestamp that cannot round-trip across JSON parsers.
23+
24+
Every timing input here is a whole second or whole millisecond.
25+
A fractional float in a serialized vector is a portability hazard.
26+
Its decimal text can parse back to a different binary value in another
27+
language, breaking cross-client determinism.
28+
The fractional part must therefore be zero.
29+
"""
30+
if not math.isfinite(timestamp) or not float(timestamp).is_integer():
31+
raise ValueError(f"slot-clock timestamp must be a whole number, got {timestamp!r}")
32+
return timestamp
33+
34+
35+
IntegralTimestamp = Annotated[float, AfterValidator(_reject_non_integral_timestamp)]
36+
"""A float timing input constrained to a whole, exactly-representable value."""
37+
38+
1939
class SlotClockConfig(StrictBaseModel):
2040
"""Timing constants every conversion in the vector assumes."""
2141

@@ -59,8 +79,8 @@ class FromUnixTime(StrictBaseModel):
5979
genesis_time: int
6080
"""Unix genesis timestamp in seconds."""
6181

62-
unix_seconds: float
63-
"""Wall-clock timestamp to convert."""
82+
unix_seconds: IntegralTimestamp
83+
"""Wall-clock timestamp to convert. Must be a whole number of seconds."""
6484

6585
def run(self) -> IntervalOutput:
6686
"""Compute intervals since genesis at the given timestamp."""
@@ -91,8 +111,8 @@ class CurrentSlot(StrictBaseModel):
91111
genesis_time: int
92112
"""Unix genesis timestamp in seconds."""
93113

94-
current_time_milliseconds: float
95-
"""Wall-clock timestamp in milliseconds."""
114+
current_time_milliseconds: IntegralTimestamp
115+
"""Wall-clock timestamp in milliseconds. Must be a whole number of milliseconds."""
96116

97117
def run(self) -> SlotOutput:
98118
"""Compute the current slot at the given timestamp."""
@@ -112,8 +132,8 @@ class CurrentInterval(StrictBaseModel):
112132
genesis_time: int
113133
"""Unix genesis timestamp in seconds."""
114134

115-
current_time_milliseconds: float
116-
"""Wall-clock timestamp in milliseconds."""
135+
current_time_milliseconds: IntegralTimestamp
136+
"""Wall-clock timestamp in milliseconds. Must be a whole number of milliseconds."""
117137

118138
def run(self) -> IntervalOutput:
119139
"""Compute the in-slot interval at the given timestamp."""
@@ -133,8 +153,8 @@ class TotalIntervals(StrictBaseModel):
133153
genesis_time: int
134154
"""Unix genesis timestamp in seconds."""
135155

136-
current_time_milliseconds: float
137-
"""Wall-clock timestamp in milliseconds."""
156+
current_time_milliseconds: IntegralTimestamp
157+
"""Wall-clock timestamp in milliseconds. Must be a whole number of milliseconds."""
138158

139159
def run(self) -> TotalIntervalsOutput:
140160
"""Compute total intervals since genesis at the given timestamp."""

0 commit comments

Comments
 (0)