Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ fix:
typecheck *args:
uv run --group lint ty check "$@"

# Detect dead code with vulture (paths and ignores configured in pyproject.toml)
[group('quality')]
deadcode *args:
uv run --group lint vulture "$@"

# Spell check source, tests, packages, and docs
[group('quality')]
spellcheck *args:
Expand Down
16 changes: 15 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ packages = ["src/lean_spec"]

[tool.ruff]
line-length = 100
# The vulture whitelist is data for dead-code detection, not executable code.
# It references symbols by bare name, which is not valid standalone Python.
extend-exclude = ["vulture_whitelist.py"]

[tool.ruff.format]
docstring-code-format = true
Expand Down Expand Up @@ -89,7 +92,7 @@ known-first-party = ["lean_spec", "consensus_testing"]
python-version = "3.12"

[tool.ty.src]
exclude = [".claude/"]
exclude = [".claude/", "vulture_whitelist.py"]

[tool.ty.rules]
# Flag any suppression comment that no longer matches a real diagnostic.
Expand Down Expand Up @@ -136,6 +139,16 @@ branch = true
[tool.coverage.report]
fail_under = 90

[tool.vulture]
# Scan source, the testing framework, the test tree, and the whitelist together.
# Most spec code is exercised only through the test suite, so the tests must be
# in scope or every test-only-used symbol shows up as a false positive.
# The whitelist names the indirectly-used symbols vulture cannot see; listing
# them explicitly keeps the ignores tight instead of relying on wide globs.
paths = ["src", "packages", "tests", "vulture_whitelist.py"]
# Show the largest unused blocks first; they are the highest-value removals.
sort_by_size = true

[tool.mdformat]
number = true
wrap = 80
Expand Down Expand Up @@ -166,6 +179,7 @@ lint = [
"ty>=0.0.1a34",
"ruff>=0.13.2,<1",
"codespell>=2.4.1,<3",
"vulture>=2.14,<3",
]
docs = [
"mkdocs>=1.6.1,<2",
Expand Down
5 changes: 0 additions & 5 deletions src/lean_spec/node/networking/gossipsub/behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,6 @@ class PeerState:
receive_task: asyncio.Task[None] | None = None
"""Task running the receive loop for this peer."""

last_rpc_time: float = 0.0
"""Timestamp of last RPC exchange."""

backoff: dict[TopicId, float] = field(default_factory=dict)
"""Per-topic backoff expiry times (from PRUNE)."""

Expand Down Expand Up @@ -919,8 +916,6 @@ async def _send_rpc(self, peer_id: PeerId, rpc: RPC) -> None:
)
peer_state.outbound_stream.write(frame)
await peer_state.outbound_stream.drain()

peer_state.last_rpc_time = time.time()
except Exception as exception:
logger.warning("Failed to send RPC to %s: %s", peer_id, exception)

Expand Down
37 changes: 0 additions & 37 deletions tests/interop/helpers/node_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,43 +528,6 @@ async def stop_all(self) -> None:
self.nodes.clear()
logger.info("All nodes stopped")

async def wait_for_finalization(
self,
target_slot: int,
timeout: float = 120.0,
poll_interval: float = 1.0,
) -> bool:
"""
Wait until all nodes finalize to at least target_slot.

Args:
target_slot: Minimum finalized slot to wait for.
timeout: Maximum wait time in seconds.
poll_interval: Time between checks.

Returns:
True if all nodes reached target, False on timeout.
"""
start = time.monotonic()

while time.monotonic() - start < timeout:
all_finalized = all(node.finalized_slot >= target_slot for node in self.nodes)

if all_finalized:
logger.info("All %d nodes finalized to slot %d", len(self.nodes), target_slot)
return True

slots = [node.finalized_slot for node in self.nodes]
logger.debug("Finalized slots: %s (target: %d)", slots, target_slot)

await asyncio.sleep(poll_interval)

slots = [node.finalized_slot for node in self.nodes]
logger.warning(
"Timeout waiting for finalization. Slots: %s (target: %d)", slots, target_slot
)
return False

async def wait_for_slot(
self,
target_slot: int,
Expand Down
3 changes: 0 additions & 3 deletions tests/node/networking/gossipsub/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ async def drain(self) -> None:
def close(self) -> None:
pass

async def wait_closed(self) -> None:
pass


def make_behavior(
d: int = 8, d_low: int = 6, d_high: int = 12, d_lazy: int = 6
Expand Down
6 changes: 1 addition & 5 deletions tests/node/networking/reqresp/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ class MockResponseStream:
errors: list[tuple[ResponseCode, str]] = field(default_factory=list)
"""Errors sent via send_error as (code, message) tuples."""

finished: bool = False
"""Whether finish() was called."""

async def send_success(self, ssz_data: bytes) -> None:
"""Record a success response."""
self.successes.append(ssz_data)
Expand All @@ -118,8 +115,7 @@ async def send_error(self, code: ResponseCode, message: str) -> None:
self.errors.append((code, message))

async def finish(self) -> None:
"""Mark stream as finished."""
self.finished = True
"""Accept the finish call."""


class TestStreamResponseAdapter:
Expand Down
7 changes: 0 additions & 7 deletions tests/node/validator/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,6 @@ def _write_manifest(path: Path, validators: list[dict[str, object]]) -> None:
)


def _write_key_files(directory: Path, indices: list[int]) -> None:
"""Write dummy SSZ key file stubs for the given validator indices."""
for i in indices:
(directory / f"att_key_{i}.ssz").write_bytes(b"att" + bytes([i]))
(directory / f"prop_key_{i}.ssz").write_bytes(b"prop" + bytes([i]))


class TestValidatorRegistryFromYaml:
"""Integration tests for the full YAML loading pipeline (files on disk -> registry)."""

Expand Down
18 changes: 8 additions & 10 deletions tests/spec/crypto/test_merkleization.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,17 @@ def test_merkleize_empty_no_limit() -> None:


@pytest.mark.parametrize(
"limit, expected_width, expected_zero_root",
"limit, expected_zero_root",
[
(0, 1, Z[0]), # limit=0 -> width=1 -> root is Z[0]
(1, 1, Z[0]), # limit=1 -> width=1 -> root is Z[0]
(2, 2, Z[1]), # limit=2 -> width=2 -> root is Z[1]
(3, 4, Z[2]), # limit=3 -> width=4 -> root is Z[2]
(7, 8, Z[3]), # limit=7 -> width=8 -> root is Z[3]
(8, 8, Z[3]),
(0, Z[0]), # limit=0 -> width=1 -> root is Z[0]
(1, Z[0]), # limit=1 -> width=1 -> root is Z[0]
(2, Z[1]), # limit=2 -> width=2 -> root is Z[1]
(3, Z[2]), # limit=3 -> width=4 -> root is Z[2]
(7, Z[3]), # limit=7 -> width=8 -> root is Z[3]
(8, Z[3]),
],
)
def test_merkleize_empty_with_limit(
limit: int, expected_width: int, expected_zero_root: Bytes32
) -> None:
def test_merkleize_empty_with_limit(limit: int, expected_zero_root: Bytes32) -> None:
"""Empty input with a limit yields the zero-subtree root at the rounded-up width."""
assert merkleize([], limit=limit) == expected_zero_root

Expand Down
13 changes: 13 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading