Skip to content

Commit 0866722

Browse files
tcoratgerclaude
andauthored
refactor: audit batch — drop dead helpers, fields, properties (leanEthereum#757)
* refactor: audit batch — drop dead helpers, fields, properties Eleven cleanups from the consensus-researcher audit, bundled. snappy: get_uncompressed_length and is_valid_compressed_data were tests-only helpers. The two corruption tests that used them are rewritten to assert decompression outcomes directly. subspecs/ssz/hash: the _htr_bytes / _htr_bytearray / _htr_memoryview register overloads existed only for one parametrize test; both leave together. Production hash_tree_root callers always pass typed SSZ containers. subspecs/xmss/rand: Rand.rho was the only Randomness-shaped helper used solely in test_message_hash; the test now builds Randomness from rand.field_elements directly. subspecs/xmss/constants: XmssConfig.PUBLIC_KEY_LEN_BYTES had a single test consumer that effectively re-derived the public-key SSZ shape. Test rewritten to compute the expected size inline as (HASH_LEN_FE + PARAMETER_LEN) * P_BYTES. subspecs/observability: get_observer was only read by tests; the private singleton is now read via observer_module._observer in the two affected tests. NullObserver is privatised to _NullObserver and dropped from the package's public re-exports. subspecs/genesis/config: num_validators field and its consistency validator were both informational; the actual count is always taken from len(genesis_validators). from_yaml was a tests-only string shortcut for model_validate(yaml.safe_load(...)); tests use a local _load helper for the same ergonomics without a public method. subspecs/validator/service: blocks_skipped_lag, attestations_skipped_lag, and duty_gate_closed were read only from tests. The private state machine stays; tests now access the underscored attributes directly. subspecs/sync/block_cache: PendingBlock.received_at was a debug-only timestamp written but never read in src. Field dropped together with the dedicated default-timestamp test; remaining test constructions no longer pass received_at. subspecs/sync/head_sync: HeadSyncResult slimmed to processed plus error. The cached, backfill_triggered, and descendants_processed fields had no readers outside their own assignments. The error field is retained per the audit note for incident-response logs. Six construction sites in head_sync.py and ~14 test assertions updated accordingly. Also drops the unused peer_id parameter on _process_cached_descendants; the rest of the file keeps peer_id where it actually drives attribution. subspecs/api/server: ApiServerConfig.enabled flag dropped (Node already gates construction). ApiServer.run no longer polls with asyncio.sleep(1); it awaits a stop event that _async_stop sets, matching the same pattern adopted earlier in live.py. Items deliberately skipped: - Container.to_hex / from_hex: both used in xmss/containers.py. - IntFieldElement protocol: removing requires Fp to grow its own Pydantic core schema; deferred as a real redesign. - The 7 Spec*Type protocols in forks/protocol.py: serve as documentation labels distinguishing block-body from block-header from aggregated-attestations etc. Collapsing to type[SpecSSZType] erased meaningful type intent. - ValidatorRegistry.primary_index: two production callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup: apply ruff format + drop HeadSyncResult fields in routing tests Two follow-ups to land the audit batch through CI. - ruff format reformatted two source files (decompress.py and the validator service) whose edits left awkward blank-line spacing. - A second test module, test_head_sync_backfill_routing.py, also constructed HeadSyncResult with the now-dropped cached, backfill_triggered, and descendants_processed fields. Stripped those argument lines so ty check passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup: restore _htr_bytes / _htr_bytearray / _htr_memoryview overloads The audit listed these as tests-only and the unit tests passed without them, but the consensus fixture filler hit "TypeError: hash_tree_root: unsupported value type bytes" the moment a lstar State container with a raw-bytes-typed field was walked. Production traversal does reach raw bytes; the three overloads are load-bearing for the fixture pipeline. Restored along with the parametrize-over-byte-likes test that was retired together with them. Verified by filling a previously failing fc test_tick_system fixture. 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 363797e commit 0866722

22 files changed

Lines changed: 65 additions & 393 deletions

src/lean_spec/snappy/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
from .decompress import (
1515
SnappyDecompressionError,
1616
decompress,
17-
get_uncompressed_length,
18-
is_valid_compressed_data,
1917
)
2018
from .framing import frame_compress, frame_decompress
2119

@@ -28,8 +26,6 @@
2826
"frame_decompress",
2927
# Utilities
3028
"max_compressed_length",
31-
"get_uncompressed_length",
32-
"is_valid_compressed_data",
3329
# Exceptions
3430
"SnappyDecompressionError",
3531
]

src/lean_spec/snappy/decompress.py

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -263,64 +263,3 @@ def _execute_copy(output: bytearray, offset: int, length: int, max_length: int)
263263
for _ in range(length):
264264
output.append(output[src_pos])
265265
src_pos += 1
266-
267-
268-
def get_uncompressed_length(data: bytes) -> int:
269-
"""Read the uncompressed length from compressed data without decompressing.
270-
271-
Args:
272-
data: Snappy-compressed bytes.
273-
274-
Returns:
275-
The declared uncompressed length.
276-
277-
Raises:
278-
SnappyDecompressionError: If the length varint is malformed.
279-
280-
Useful for:
281-
- Pre-allocating buffers before decompression.
282-
- Quick validation of compressed data.
283-
- Checking if you have enough memory for decompression.
284-
"""
285-
if not data:
286-
raise SnappyDecompressionError("Empty input")
287-
288-
try:
289-
length, _ = decode_varint32(data, 0)
290-
return length
291-
except ValueError as e:
292-
raise SnappyDecompressionError(f"Invalid length varint: {e}") from e
293-
294-
295-
def is_valid_compressed_data(data: bytes) -> bool:
296-
"""Check if data appears to be valid Snappy-compressed data.
297-
298-
Args:
299-
data: Data to check.
300-
301-
Returns:
302-
True if the data appears to be valid Snappy format.
303-
304-
This performs quick validation WITHOUT full decompression:
305-
1. Checks that the length varint is valid.
306-
2. Verifies there's data after the varint (unless length is 0).
307-
308-
Use this for fast rejection of obviously invalid data.
309-
310-
Note:
311-
This does NOT guarantee the data will decompress successfully.
312-
It only checks the header. The compressed content may still
313-
be corrupted.
314-
"""
315-
if not data:
316-
return False
317-
318-
try:
319-
length, varint_bytes = decode_varint32(data, 0)
320-
321-
# Sanity checks:
322-
# - If length = 0, data is valid (empty original).
323-
# - If length > 0, there must be compressed data after the varint.
324-
return length == 0 or varint_bytes < len(data)
325-
except ValueError:
326-
return False

src/lean_spec/subspecs/api/server.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,6 @@ class ApiServerConfig:
4545
port: int = 5052
4646
"""Port to listen on."""
4747

48-
enabled: bool = True
49-
"""Whether the API server is enabled."""
50-
5148

5249
@dataclass(slots=True)
5350
class ApiServer:
@@ -85,17 +82,16 @@ class ApiServer:
8582
_site: web.TCPSite | None = field(default=None, init=False)
8683
"""TCP site for the server."""
8784

85+
_stop_event: asyncio.Event = field(default_factory=asyncio.Event, init=False)
86+
"""Set when the server stops, so run() can return without polling."""
87+
8888
@property
8989
def store(self) -> Store | None:
9090
"""Get the current Store instance."""
9191
return self.store_getter() if self.store_getter else None
9292

9393
async def start(self) -> None:
9494
"""Start the API server in the background."""
95-
if not self.config.enabled:
96-
logger.info("API server is disabled")
97-
return
98-
9995
app = web.Application()
10096

10197
# Store the store_getter in app for handlers that need store access
@@ -126,9 +122,7 @@ async def run(self) -> None:
126122
Blocks until stop() is called.
127123
"""
128124
await self.start()
129-
130-
while self._runner is not None:
131-
await asyncio.sleep(1)
125+
await self._stop_event.wait()
132126

133127
def stop(self) -> None:
134128
"""Request graceful shutdown (fire-and-forget). Prefer aclose() in async code."""
@@ -146,3 +140,4 @@ async def _async_stop(self) -> None:
146140
self._runner = None
147141
self._site = None
148142
logger.info("API server stopped")
143+
self._stop_event.set()

src/lean_spec/subspecs/genesis/config.py

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from typing import Any
1919

2020
import yaml
21-
from pydantic import Field, field_validator, model_validator
21+
from pydantic import Field, field_validator
2222

2323
from lean_spec.forks import Validator, Validators
2424
from lean_spec.types import Bytes52, StrictBaseModel, Uint64, ValidatorIndex
@@ -79,14 +79,6 @@ class GenesisConfig(StrictBaseModel):
7979
Immutable once the chain launches.
8080
"""
8181

82-
num_validators: Uint64 | None = Field(default=None, alias="NUM_VALIDATORS")
83-
"""
84-
Number of validators (optional).
85-
86-
This field is informational and may be included in config files.
87-
The actual validator count is derived from the genesis validator list.
88-
"""
89-
9082
genesis_validators: list[GenesisValidatorEntry] = Field(alias="GENESIS_VALIDATORS")
9183
"""
9284
Validators trusted to secure the chain from slot 0.
@@ -99,18 +91,6 @@ class GenesisConfig(StrictBaseModel):
9991
Security note: 2/3+ collusion controls the chain until new validators join.
10092
"""
10193

102-
@model_validator(mode="after")
103-
def validate_num_validators_consistency(self) -> GenesisConfig:
104-
"""Verify num_validators matches actual count when provided."""
105-
if self.num_validators is not None:
106-
actual_count = len(self.genesis_validators)
107-
if int(self.num_validators) != actual_count:
108-
raise ValueError(
109-
f"NUM_VALIDATORS ({self.num_validators}) does not match "
110-
f"actual validator count ({actual_count})"
111-
)
112-
return self
113-
11494
def to_validators(self) -> Validators:
11595
"""
11696
Build the genesis validator set with assigned indices.
@@ -146,13 +126,3 @@ def from_yaml_file(cls, path: Path | str) -> GenesisConfig:
146126
with path.open(encoding="utf-8") as f:
147127
data = yaml.safe_load(f)
148128
return cls.model_validate(data)
149-
150-
@classmethod
151-
def from_yaml(cls, content: str) -> GenesisConfig:
152-
"""
153-
Load configuration from a YAML string.
154-
155-
Useful for testing or programmatic config generation.
156-
"""
157-
data = yaml.safe_load(content)
158-
return cls.model_validate(data)

src/lean_spec/subspecs/observability/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,15 @@
1212
"""
1313

1414
from .observer import (
15-
NullObserver,
1615
SpecObserver,
17-
get_observer,
1816
observe_on_attestation,
1917
observe_on_block,
2018
observe_state_transition,
2119
set_observer,
2220
)
2321

2422
__all__ = [
25-
"NullObserver",
2623
"SpecObserver",
27-
"get_observer",
2824
"observe_on_attestation",
2925
"observe_on_block",
3026
"observe_state_transition",

src/lean_spec/subspecs/observability/observer.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def on_attestation_timed(self, seconds: float) -> None:
4242
"""Report the wall time of validating and integrating a gossip attestation."""
4343

4444

45-
class NullObserver:
45+
class _NullObserver:
4646
"""
4747
Default observer that discards every event.
4848
@@ -60,11 +60,11 @@ def on_attestation_timed(self, seconds: float) -> None: # noqa: ARG002
6060
"""Accept and discard."""
6161

6262

63-
_observer: SpecObserver = NullObserver()
63+
_observer: SpecObserver = _NullObserver()
6464
"""
6565
Process-wide observer singleton.
6666
67-
Starts as a NullObserver so spec imports are side-effect-free.
67+
Starts as a _NullObserver so spec imports are side-effect-free.
6868
Replaced by the client at startup via set_observer.
6969
"""
7070

@@ -81,16 +81,6 @@ def set_observer(observer: SpecObserver) -> None:
8181
_observer = observer
8282

8383

84-
def get_observer() -> SpecObserver:
85-
"""
86-
Return the currently registered observer.
87-
88-
Spec code calls this to publish events.
89-
When no observer has been registered the returned value is a NullObserver.
90-
"""
91-
return _observer
92-
93-
9484
@contextmanager
9585
def observe_state_transition() -> Iterator[None]:
9686
"""

src/lean_spec/subspecs/sync/block_cache.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141

4242
from collections import OrderedDict, defaultdict
4343
from dataclasses import dataclass, field
44-
from time import time
4544

4645
from lean_spec.forks import SignedBlock, Store
4746
from lean_spec.subspecs.networking.transport.peer_id import PeerId
@@ -110,15 +109,6 @@ class PendingBlock:
110109
None for self-produced blocks.
111110
"""
112111

113-
received_at: float = field(default_factory=time)
114-
"""
115-
Unix timestamp when the block was received.
116-
117-
Enables staleness detection and debugging.
118-
119-
Very old pending blocks may indicate a stuck backfill or network issues.
120-
"""
121-
122112
backfill_depth: int = 0
123113
"""
124114
Depth of backfill chain from original request.

src/lean_spec/subspecs/sync/head_sync.py

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -64,22 +64,13 @@ class HeadSyncResult:
6464
"""
6565
Result of processing a gossip block.
6666
67-
Provides detailed feedback about what happened when a block was received.
68-
This allows the SyncService to make informed decisions about state.
67+
The SyncService only branches on the processed flag; the error string is
68+
retained for incident-response logs and post-mortem inspection.
6969
"""
7070

7171
processed: bool
7272
"""True if the block was immediately integrated into the Store."""
7373

74-
cached: bool
75-
"""True if the block was added to the cache (parent unknown)."""
76-
77-
backfill_triggered: bool
78-
"""True if backfill was initiated for missing parents."""
79-
80-
descendants_processed: int
81-
"""Number of cached descendants that were also processed."""
82-
8374
error: str | None = None
8475
"""Error message if processing failed."""
8576

@@ -177,19 +168,13 @@ async def on_gossip_block(
177168
logger.debug("on_gossip_block: skipping - already processing")
178169
return HeadSyncResult(
179170
processed=False,
180-
cached=False,
181-
backfill_triggered=False,
182-
descendants_processed=0,
183171
), store
184172

185173
# Skip if already in store (duplicate).
186174
if block_root in store.blocks:
187175
logger.debug("on_gossip_block: skipping - already in store")
188176
return HeadSyncResult(
189177
processed=False,
190-
cached=False,
191-
backfill_triggered=False,
192-
descendants_processed=0,
193178
), store
194179

195180
# Check if parent exists in store.
@@ -255,24 +240,17 @@ async def _process_block_with_descendants(
255240
)
256241
return HeadSyncResult(
257242
processed=False,
258-
cached=False,
259-
backfill_triggered=False,
260-
descendants_processed=0,
261243
error=str(e),
262244
), store
263245

264246
# Process cached descendants.
265-
descendants_count, store = await self._process_cached_descendants(
247+
_, store = await self._process_cached_descendants(
266248
parent_root=block_root,
267249
store=store,
268-
peer_id=peer_id,
269250
)
270251

271252
return HeadSyncResult(
272253
processed=True,
273-
cached=False,
274-
backfill_triggered=False,
275-
descendants_processed=descendants_count,
276254
), store
277255

278256
finally:
@@ -282,7 +260,6 @@ async def _process_cached_descendants(
282260
self,
283261
parent_root: Bytes32,
284262
store: Store,
285-
peer_id: PeerId | None,
286263
) -> tuple[int, Store]:
287264
"""
288265
Process any cached blocks that descend from the given parent.
@@ -296,7 +273,6 @@ async def _process_cached_descendants(
296273
Args:
297274
parent_root: Root of the parent block just processed.
298275
store: Current store (may be updated during processing).
299-
peer_id: Peer ID for error attribution.
300276
301277
Returns:
302278
Tuple of (descendants successfully processed, updated store).
@@ -331,7 +307,6 @@ async def _process_cached_descendants(
331307
desc_count, store = await self._process_cached_descendants(
332308
parent_root=child_root,
333309
store=store,
334-
peer_id=peer_id,
335310
)
336311
processed_count += desc_count
337312

@@ -382,9 +357,6 @@ async def _cache_and_backfill(
382357
)
383358
return HeadSyncResult(
384359
processed=False,
385-
cached=False,
386-
backfill_triggered=False,
387-
descendants_processed=0,
388360
), store
389361

390362
# Add to cache.
@@ -427,9 +399,6 @@ async def _cache_and_backfill(
427399

428400
return HeadSyncResult(
429401
processed=False,
430-
cached=True,
431-
backfill_triggered=True,
432-
descendants_processed=0,
433402
), store
434403

435404
def reset(self) -> None:

0 commit comments

Comments
 (0)