Skip to content

Commit cc93e09

Browse files
authored
primitives: limit Uint to int conversions (leanEthereum#225)
* primitives: limit Uint to int conversions * touchups * fix * fix linter
1 parent 4b3e905 commit cc93e09

8 files changed

Lines changed: 90 additions & 41 deletions

File tree

src/lean_spec/subspecs/containers/block/block.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,11 @@ def verify_signatures(self, parent_state: "State") -> bool:
163163

164164
# Verify each attestation signature
165165
for attestation, signature in zip(all_attestations, signatures, strict=True):
166-
# Identify the validator who created this attestation
167-
validator_id = attestation.validator_id.as_int()
168-
169166
# Ensure validator exists in the active set
170-
assert validator_id < len(validators), "Validator index out of range"
171-
validator = cast(Validator, validators[validator_id])
167+
assert attestation.validator_id < Uint64(len(validators)), (
168+
"Validator index out of range"
169+
)
170+
validator = cast(Validator, validators[attestation.validator_id])
172171

173172
# Verify the XMSS signature
174173
#

src/lean_spec/subspecs/containers/slot.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def is_justifiable_after(self, finalized_slot: Slot) -> bool:
3535
assert self >= finalized_slot, "Candidate slot must not be before finalized slot"
3636

3737
# Calculate the distance in slots from the last finalized slot.
38-
delta = (self - finalized_slot).as_int()
38+
# Convert to int for pure arithmetic operations below.
39+
delta = int(self - finalized_slot)
3940

4041
return (
4142
# Rule 1: The first 5 slots after finalization are always justifiable.

src/lean_spec/subspecs/containers/state/state.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def process_block_header(self, block: Block) -> "State":
279279
# If slots were skipped (missed proposals), we must record them.
280280
#
281281
# Formula: (Current - Parent - 1). Adjacent blocks have a gap of 0.
282-
num_empty_slots = (block.slot - parent_header.slot - Slot(1)).as_int()
282+
num_empty_slots = int(block.slot - parent_header.slot - Slot(1))
283283

284284
# Update the list of historical block roots.
285285
#
@@ -425,27 +425,25 @@ def process_attestations(
425425
# Ignore attestations whose source is not already justified,
426426
# or whose target is not in the history, or whose target is not a
427427
# valid justifiable slot
428-
source_slot = source.slot.as_int()
429-
target_slot = target.slot.as_int()
430428

431429
# Source slot must be justified
432-
if not justified_slots[source_slot]:
430+
if not justified_slots[source.slot]:
433431
continue
434432

435433
# Target slot must not be already justified
436434
# This condition is missing in 3sf mini but has been added here because
437435
# we don't want to re-introduce the target again for remaining votes if
438436
# the slot is already justified and its tracking already cleared out
439437
# from justifications map
440-
if justified_slots[target_slot]:
438+
if justified_slots[target.slot]:
441439
continue
442440

443441
# Source root must match the state's historical block hashes
444-
if source.root != self.historical_block_hashes[source_slot]:
442+
if source.root != self.historical_block_hashes[source.slot]:
445443
continue
446444

447445
# Target root must match the state's historical block hashes
448-
if target.root != self.historical_block_hashes[target_slot]:
446+
if target.root != self.historical_block_hashes[target.slot]:
449447
continue
450448

451449
# Target slot must be after source slot
@@ -460,9 +458,8 @@ def process_attestations(
460458
if target.root not in justifications:
461459
justifications[target.root] = [Boolean(False)] * self.validators.count
462460

463-
validator_id = attestation.validator_id.as_int()
464-
if not justifications[target.root][validator_id]:
465-
justifications[target.root][validator_id] = Boolean(True)
461+
if not justifications[target.root][attestation.validator_id]:
462+
justifications[target.root][attestation.validator_id] = Boolean(True)
466463

467464
count = sum(bool(justified) for justified in justifications[target.root])
468465

@@ -473,14 +470,14 @@ def process_attestations(
473470
# justifying specially if the num_validators is low in testing scenarios
474471
if 3 * count >= (2 * self.validators.count):
475472
latest_justified = target
476-
justified_slots[target_slot] = True
473+
justified_slots[target.slot] = True
477474
del justifications[target.root]
478475

479476
# Finalization: if the target is the next valid justifiable
480477
# hash after the source
481478
if not any(
482479
Slot(slot).is_justifiable_after(self.latest_finalized.slot)
483-
for slot in range(source_slot + 1, target_slot)
480+
for slot in range(source.slot + Slot(1), target.slot)
484481
):
485482
latest_finalized = source
486483

src/lean_spec/subspecs/containers/state/types.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,33 @@ class HistoricalBlockHashes(SSZList):
1111
"""List of historical block root hashes up to historical_roots_limit."""
1212

1313
ELEMENT_TYPE = Bytes32
14-
LIMIT = DEVNET_CONFIG.historical_roots_limit.as_int()
14+
LIMIT = int(DEVNET_CONFIG.historical_roots_limit)
1515

1616

1717
class JustificationRoots(SSZList):
1818
"""List of justified block roots up to historical_roots_limit."""
1919

2020
ELEMENT_TYPE = Bytes32
21-
LIMIT = DEVNET_CONFIG.historical_roots_limit.as_int()
21+
LIMIT = int(DEVNET_CONFIG.historical_roots_limit)
2222

2323

2424
class JustifiedSlots(BaseBitlist):
2525
"""Bitlist tracking justified slots up to historical roots limit."""
2626

27-
LIMIT = DEVNET_CONFIG.historical_roots_limit.as_int()
27+
LIMIT = int(DEVNET_CONFIG.historical_roots_limit)
2828

2929

3030
class JustificationValidators(BaseBitlist):
3131
"""Bitlist for tracking validator justifications per historical root."""
3232

33-
LIMIT = (
34-
DEVNET_CONFIG.historical_roots_limit.as_int()
35-
* DEVNET_CONFIG.validator_registry_limit.as_int()
36-
)
33+
LIMIT = int(DEVNET_CONFIG.historical_roots_limit) * int(DEVNET_CONFIG.validator_registry_limit)
3734

3835

3936
class Validators(SSZList):
4037
"""Validator registry tracked in the state."""
4138

4239
ELEMENT_TYPE = Validator
43-
LIMIT = DEVNET_CONFIG.validator_registry_limit.as_int()
40+
LIMIT = int(DEVNET_CONFIG.validator_registry_limit)
4441

4542
@property
4643
def count(self) -> int:

src/lean_spec/subspecs/networking/gossipsub/parameters.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ class GossipsubParameters(StrictBaseModel):
3939
"""The number of history windows to gossip about."""
4040

4141
seen_ttl_secs: int = (
42-
DEVNET_CONFIG.seconds_per_slot.as_int()
43-
* DEVNET_CONFIG.justification_lookback_slots.as_int()
44-
* 2
42+
int(DEVNET_CONFIG.seconds_per_slot) * int(DEVNET_CONFIG.justification_lookback_slots) * 2
4543
)
4644
"""
4745
The expiry time in seconds for the cache of seen message IDs.

src/lean_spec/types/uint.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,6 @@ def max_value(cls) -> Self:
170170
"""The maximum value for this unsigned integer."""
171171
return cls(2**cls.BITS - 1)
172172

173-
def as_int(self) -> int:
174-
"""Convert the unsigned integer to a plain integer."""
175-
return int(self)
176-
177173
def to_bytes(
178174
self,
179175
length: SupportsIndex | None = None,
@@ -392,6 +388,10 @@ def __hash__(self) -> int:
392388
"""Return a distinct hash for the object."""
393389
return hash((type(self), int(self)))
394390

391+
def __index__(self) -> int:
392+
"""Return self as an integer for use in slicing and indexing."""
393+
return int(self)
394+
395395

396396
class Uint8(BaseUint):
397397
"""A type representing an 8-bit unsigned integer (uint8)."""

tests/lean_spec/subspecs/forkchoice/test_time_management.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ def test_on_tick_already_current(self, sample_store: Store) -> None:
9393
# Try to advance to current time (should be no-op)
9494
sample_store = sample_store.on_tick(current_target, has_proposal=True)
9595

96-
# Should not change significantly
97-
assert abs(sample_store.time.as_int() - initial_time.as_int()) <= 10 # small tolerance
96+
# Should not change significantly (time can only increase)
97+
assert sample_store.time - initial_time <= Uint64(10) # small tolerance
9898

9999
def test_on_tick_small_increment(self, sample_store: Store) -> None:
100100
"""Test on_tick with small time increment."""
@@ -156,12 +156,12 @@ def test_tick_interval_actions_by_phase(self, sample_store: Store) -> None:
156156
)
157157

158158
# Tick through a complete slot cycle
159-
for interval in range(INTERVALS_PER_SLOT.as_int()):
159+
for interval in range(INTERVALS_PER_SLOT):
160160
has_proposal = interval == 0 # Proposal only in first interval
161161
sample_store = sample_store.tick_interval(has_proposal=has_proposal)
162162

163163
current_interval = sample_store.time % INTERVALS_PER_SLOT
164-
expected_interval = Uint64((interval + 1) % INTERVALS_PER_SLOT.as_int())
164+
expected_interval = Uint64((interval + 1)) % INTERVALS_PER_SLOT
165165
assert current_interval == expected_interval
166166

167167

@@ -175,15 +175,15 @@ def test_slot_to_time_conversion(self, sample_config: Config) -> None:
175175
genesis_time = sample_config.genesis_time
176176

177177
# Slot 0 should be at genesis time
178-
slot_0_time = genesis_time + Uint64(0 * SECONDS_PER_SLOT.as_int())
178+
slot_0_time = genesis_time + Uint64(0) * SECONDS_PER_SLOT
179179
assert slot_0_time == genesis_time
180180

181181
# Slot 1 should be at genesis + SECONDS_PER_SLOT
182-
slot_1_time = genesis_time + Uint64(1 * SECONDS_PER_SLOT.as_int())
182+
slot_1_time = genesis_time + Uint64(1) * SECONDS_PER_SLOT
183183
assert slot_1_time == genesis_time + SECONDS_PER_SLOT
184184

185185
# Slot 10 should be at genesis + 10 * SECONDS_PER_SLOT
186-
slot_10_time = genesis_time + Uint64(10 * SECONDS_PER_SLOT.as_int())
186+
slot_10_time = genesis_time + Uint64(10) * SECONDS_PER_SLOT
187187
assert slot_10_time == genesis_time + Uint64(10) * SECONDS_PER_SLOT
188188

189189
def test_time_to_slot_conversion(self, sample_config: Config) -> None:

tests/lean_spec/types/test_uint.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def test_arithmetic_operators(uint_class: Type[BaseUint]) -> None:
135135
assert uint_class(b_val) ** 4 == uint_class(b_val**4)
136136
if uint_class.BITS <= 16: # Pow gets too big quickly
137137
with pytest.raises(OverflowError):
138-
_ = a ** b.as_int()
138+
_ = a ** int(b)
139139

140140

141141
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
@@ -241,6 +241,63 @@ def test_hash(uint_class: Type[BaseUint]) -> None:
241241
assert hash(uint_class(1)) != hash(uint_class(2))
242242

243243

244+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
245+
def test_index_list_access(uint_class: Type[BaseUint]) -> None:
246+
"""Tests that Uint types can be used directly for list indexing."""
247+
data = ["a", "b", "c", "d", "e"]
248+
idx = uint_class(2)
249+
assert data[idx] == "c"
250+
assert data[uint_class(0)] == "a"
251+
assert data[uint_class(4)] == "e"
252+
253+
254+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
255+
def test_index_slicing(uint_class: Type[BaseUint]) -> None:
256+
"""Tests that Uint types can be used in slice operations."""
257+
data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
258+
start = uint_class(2)
259+
stop = uint_class(7)
260+
step = uint_class(2)
261+
262+
assert data[start:stop] == [2, 3, 4, 5, 6]
263+
assert data[:stop] == [0, 1, 2, 3, 4, 5, 6]
264+
assert data[start:] == [2, 3, 4, 5, 6, 7, 8, 9]
265+
assert data[start:stop:step] == [2, 4, 6]
266+
267+
268+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
269+
def test_index_range(uint_class: Type[BaseUint]) -> None:
270+
"""Tests that Uint types can be used in range()."""
271+
n = uint_class(5)
272+
result = list(range(n))
273+
assert result == [0, 1, 2, 3, 4]
274+
275+
start = uint_class(2)
276+
stop = uint_class(8)
277+
step = uint_class(2)
278+
result = list(range(start, stop, step))
279+
assert result == [2, 4, 6]
280+
281+
282+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
283+
def test_index_hex_bin_oct(uint_class: Type[BaseUint]) -> None:
284+
"""Tests that Uint types work with hex(), bin(), oct()."""
285+
val = uint_class(42)
286+
assert hex(val) == "0x2a"
287+
assert bin(val) == "0b101010"
288+
assert oct(val) == "0o52"
289+
290+
291+
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
292+
def test_index_operator_index(uint_class: Type[BaseUint]) -> None:
293+
"""Tests that operator.index() works with Uint types."""
294+
import operator
295+
296+
val = uint_class(42)
297+
assert operator.index(val) == 42
298+
assert isinstance(operator.index(val), int)
299+
300+
244301
@pytest.mark.parametrize("uint_class", ALL_UINT_TYPES)
245302
def test_to_bytes_default(uint_class: Type[BaseUint]) -> None:
246303
"""Tests the default behavior of the to_bytes method."""

0 commit comments

Comments
 (0)