Skip to content

Commit 3b45c3d

Browse files
tcoratgerclaude
andauthored
refactor(types/uint): polish base uint type (leanEthereum#769)
* fix(types/uint): enforce strict same-type equality across widths The hash mixes in the concrete subclass, so allowing cross-width equality broke the hash/eq contract: Uint8(5) == Uint16(5) returned True while hash(Uint8(5)) != hash(Uint16(5)), silently corrupting set and dict membership. Tighten __eq__ and __ne__ to require type(other) is type(self) and add parametrized tests covering every ordered pair of distinct widths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(types/uint): require exact-same-concrete-type on all binary ops Every BaseUint binary operator now uses type(other) is type(self) instead of isinstance(other, BaseUint). Previously, Uint8(5) + Uint16(10) silently produced Uint8(15) because the result type defaulted to the LHS. Now any cross-width or cross-newtype mixing raises TypeError, matching how a strongly typed language rejects u8 + u16 or Slot + Uint64. Also in this commit: - __rpow__ gains the missing mod parameter for 3-arg pow. - __pow__ uses overloads to express the parent's two-overload return shape. - Dead _validate_int_operand helper removed (no callers remain). - Class docstring documents the universal strict-type rule. Consumers throughout the chain/clock, fork-choice, validator service, and xmss code that were silently relying on cross-type interop are updated to convert explicitly via int() or the appropriate newtype constructor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(types/uint): tighten docstrings and comments per /doc rules Strip backticks, drop restated-code prose, collapse multi-line docstrings to single-sentence summaries where the signature already documents the rest, and fix two factually wrong Raises sections that cited non-existent error types. Add brief Why comments to the Pydantic schema body where the plumbing is non-obvious. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(types/uint): full-message error matching and 100% coverage Every pytest.raises now anchors on the exact exception message via re.escape, replacing partial or wildcard matches that could silently accept the wrong error. Add tests for the previously-untested three-argument reverse pow paths (both the success path and the modulo type-rejection branch), lifting uint.py coverage to 100%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(chain/clock): trim redundant strict-typing comment in from_slot Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(testing/store_checks): type StoreChecks.time as Interval Store.time is an Interval; the StoreChecks comparison field was still typed as Uint64, which under the strict-typing rule causes Interval != Uint64 to raise TypeError during fixture validation. Update the field to Interval and the two test call sites that constructed Uint64 values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3c61690 commit 3b45c3d

24 files changed

Lines changed: 454 additions & 353 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
MILLISECONDS_PER_INTERVAL,
1414
SECONDS_PER_SLOT,
1515
)
16-
from lean_spec.types import Uint64
16+
from lean_spec.types import Slot, Uint64
1717

1818
from .base import BaseConsensusFixture
1919

@@ -72,7 +72,7 @@ def _make_from_unix_time(self) -> dict[str, Any]:
7272

7373
def _make_from_slot(self) -> dict[str, Any]:
7474
"""Convert slot number to interval at that slot's start."""
75-
slot = Uint64(self.input["slot"])
75+
slot = Slot(self.input["slot"])
7676
interval = Interval.from_slot(slot)
7777
return {"interval": int(interval)}
7878

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from lean_spec.forks.lstar.containers.block.block import Block
77
from lean_spec.forks.lstar.spec import LstarSpec
88
from lean_spec.forks.lstar.store import Store
9+
from lean_spec.subspecs.chain.clock import Interval
910
from lean_spec.subspecs.ssz import hash_tree_root
10-
from lean_spec.types import ZERO_HASH, Bytes32, CamelModel, Slot, Uint64, ValidatorIndex
11+
from lean_spec.types import ZERO_HASH, Bytes32, CamelModel, Slot, ValidatorIndex
1112

1213
from .utils import resolve_block_root
1314

@@ -123,7 +124,7 @@ class StoreChecks(CamelModel):
123124
This allows tests to focus on the properties they care about.
124125
"""
125126

126-
time: Uint64 | None = None
127+
time: Interval | None = None
127128
"""Expected store time (in intervals since genesis)."""
128129

129130
head_slot: Slot | None = None

src/lean_spec/forks/lstar/spec.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,8 @@ def validate_attestation(self, store: LstarStore, attestation_data: AttestationD
10921092
# let an adversary pre-publish next-slot aggregates ahead of any
10931093
# honest validator.
10941094
attestation_start_interval = Interval.from_slot(data.slot)
1095-
assert attestation_start_interval <= store.time + GOSSIP_DISPARITY_INTERVALS, (
1095+
gossip_disparity = Interval(int(GOSSIP_DISPARITY_INTERVALS))
1096+
assert attestation_start_interval <= store.time + gossip_disparity, (
10961097
"Attestation too far in future"
10971098
)
10981099

@@ -1761,7 +1762,7 @@ def tick_interval(
17611762
"""
17621763
# Advance time by one interval
17631764
store = store.model_copy(update={"time": store.time + Interval(1)})
1764-
current_interval = store.time % INTERVALS_PER_SLOT
1765+
current_interval = Interval(int(store.time) % int(INTERVALS_PER_SLOT))
17651766
new_aggregates: list[SignedAggregatedAttestation] = []
17661767

17671768
if current_interval == Interval(0) and has_proposal:

src/lean_spec/subspecs/chain/clock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def from_unix_time(cls, unix_seconds: Uint64, genesis_time: Uint64) -> Interval:
5353
return cls(delta_ms // MILLISECONDS_PER_INTERVAL)
5454

5555
@classmethod
56-
def from_slot(cls, slot: Uint64) -> Interval:
56+
def from_slot(cls, slot: Slot) -> Interval:
5757
"""
5858
Convert a slot number to the interval at that slot's start.
5959
@@ -67,7 +67,7 @@ def from_slot(cls, slot: Uint64) -> Interval:
6767
Interval count at the start of the given slot.
6868
"""
6969
# Slot boundaries fall on exact multiples of the interval count.
70-
return cls(slot * INTERVALS_PER_SLOT)
70+
return cls(int(slot) * int(INTERVALS_PER_SLOT))
7171

7272

7373
@dataclass(frozen=True, slots=True)

src/lean_spec/subspecs/chain/service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from lean_spec.forks import LstarSpec, SignedAggregatedAttestation
3030
from lean_spec.subspecs.chain.config import INTERVALS_PER_SLOT
3131
from lean_spec.subspecs.sync import SyncService
32-
from lean_spec.types import Uint64
3332

3433
from .clock import Interval, SlotClock
3534

@@ -167,9 +166,10 @@ async def _tick_to(self, target_interval: Interval) -> list[SignedAggregatedAtte
167166
# Jump to the last full slot boundary before the target.
168167
# The final slot's worth of intervals still runs normally so that
169168
# aggregation, safe target, and attestation acceptance happen.
169+
intervals_per_slot = Interval(int(INTERVALS_PER_SLOT))
170170
gap = target_interval - store.time
171-
if gap > INTERVALS_PER_SLOT:
172-
skip_to = Uint64(target_interval - INTERVALS_PER_SLOT)
171+
if gap > intervals_per_slot:
172+
skip_to = target_interval - intervals_per_slot
173173
store = store.model_copy(update={"time": skip_to})
174174
self.sync_service.store = store
175175

src/lean_spec/subspecs/validator/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ async def run(self) -> None:
165165
my_indices,
166166
)
167167

168-
if interval == Uint64(0):
168+
if interval == Interval(0):
169169
# Block production interval.
170170
#
171171
# Check if any of our validators is the proposer.
@@ -204,7 +204,7 @@ async def run(self) -> None:
204204
# Why split eligibility from the sync gate: the skip counter
205205
# must only tick on real misses, never on wrong-interval
206206
# iterations.
207-
needs_attestation = interval >= Uint64(1) and slot not in self._attested_slots
207+
needs_attestation = interval >= Interval(1) and slot not in self._attested_slots
208208
if needs_attestation:
209209
logger.debug(
210210
"ValidatorService: producing attestations for slot %d (interval %d)",

src/lean_spec/subspecs/xmss/interface.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def key_gen(self, activation_slot: Slot, num_active_slots: Uint64) -> KeyPair:
117117
config = self.config
118118

119119
# Ensure the requested activation range is within the scheme's total supported lifetime.
120-
if activation_slot + num_active_slots > config.LIFETIME:
120+
if int(activation_slot) + int(num_active_slots) > int(config.LIFETIME):
121121
raise ValueError("Activation range exceeds the key's lifetime.")
122122

123123
# Generate the random public parameter `P` and the master PRF key.
@@ -330,7 +330,7 @@ def sign(self, sk: SecretKey, slot: Slot, message: Bytes32) -> Signature:
330330
bottom_tree = sk.left_bottom_tree if slot_int < boundary else sk.right_bottom_tree
331331

332332
# Generate the combined authentication path
333-
path = combined_path(sk.top_tree, bottom_tree, slot)
333+
path = combined_path(sk.top_tree, bottom_tree, Uint64(int(slot)))
334334

335335
# Assemble and return the final signature, which contains:
336336
# - The OTS,
@@ -385,7 +385,7 @@ def verify(self, pk: PublicKey, slot: Slot, message: Bytes32, sig: Signature) ->
385385
#
386386
# Return False instead of raising to avoid panic on invalid signatures.
387387
# The slot is attacker-controlled input.
388-
if slot >= self.config.LIFETIME:
388+
if int(slot) >= int(self.config.LIFETIME):
389389
return False
390390

391391
# Re-encode the message using the randomness `rho` from the signature.

0 commit comments

Comments
 (0)