From 1cc56a66728cc2da159d2c22949b68f91aeceb12 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Thu, 28 May 2026 11:42:38 +0200 Subject: [PATCH] revert(forks): drop SigScheme capability and @requires marker (#715) Reverts the fork-level capability Protocol and pytest marker introduced in #715. The abstraction is not needed at this stage; signature verification reads TARGET_SIGNATURE_SCHEME directly. - Delete forks/capabilities.py (SigScheme Protocol) - Drop sig_scheme ClassVar from LstarSpec; on_gossip_attestation now references TARGET_SIGNATURE_SCHEME directly - Delete framework/markers.py (requires_capability helper) - Strip requires marker registration, spec-instance cache, and capability-check branch from the filler plugin - Drop valid_from / valid_at / requires entries from pyproject markers - Remove the two test files that exercised the capability and marker verify_signatures and on_block already lost their scheme-threading in \#717 (TypeTwoMultiSignature rewrite), so the old scheme= parameters and the LEAN_ENV_TO_SCHEMES filler overrides are not reintroduced. Co-Authored-By: Claude Opus 4.7 --- packages/testing/src/framework/__init__.py | 4 - packages/testing/src/framework/markers.py | 34 --- .../src/framework/pytest_plugins/filler.py | 37 +--- pyproject.toml | 3 - src/lean_spec/forks/__init__.py | 4 - src/lean_spec/forks/capabilities.py | 16 -- src/lean_spec/forks/lstar/spec.py | 12 +- .../consensus/lstar/test_capability_gating.py | 39 ---- tests/lean_spec/forks/test_capabilities.py | 201 ------------------ 9 files changed, 6 insertions(+), 344 deletions(-) delete mode 100644 packages/testing/src/framework/markers.py delete mode 100644 src/lean_spec/forks/capabilities.py delete mode 100644 tests/consensus/lstar/test_capability_gating.py delete mode 100644 tests/lean_spec/forks/test_capabilities.py diff --git a/packages/testing/src/framework/__init__.py b/packages/testing/src/framework/__init__.py index 224576179..9addb6831 100644 --- a/packages/testing/src/framework/__init__.py +++ b/packages/testing/src/framework/__init__.py @@ -4,7 +4,3 @@ This module provides base classes and utilities that are common across both consensus and execution layer testing. """ - -from .markers import requires_capability - -__all__ = ["requires_capability"] diff --git a/packages/testing/src/framework/markers.py b/packages/testing/src/framework/markers.py deleted file mode 100644 index 9624eba24..000000000 --- a/packages/testing/src/framework/markers.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Helper for the capability-requirement pytest marker.""" - -import pytest - - -def requires_capability(*capabilities: type) -> pytest.MarkDecorator: - """Build a capability-requirement marker over one or more Protocols. - - Why a helper is needed at all: - - - Pytest treats a single class argument to a marker as the - thing being decorated, not as marker data. - - That makes pytest try to instantiate the class. - - Protocols can't be instantiated, so applying the marker - directly to a Protocol raises TypeError at import. - - What this helper does: - - - Passes the capability through as marker data instead of as - the decoration target. - - Validates each argument up front, so non-Protocol classes - and Protocols missing the runtime-checkable decorator fail - at import rather than at test collection. - - Raises: - TypeError: If any argument is not a runtime-checkable Protocol. - """ - for cap in capabilities: - if not getattr(cap, "_is_runtime_protocol", False): - raise TypeError( - f"requires_capability expects @runtime_checkable Protocols; " - f"got {getattr(cap, '__name__', cap)!r}" - ) - return pytest.mark.requires.with_args(*capabilities) diff --git a/packages/testing/src/framework/pytest_plugins/filler.py b/packages/testing/src/framework/pytest_plugins/filler.py index 67748d8ac..af5eec73a 100644 --- a/packages/testing/src/framework/pytest_plugins/filler.py +++ b/packages/testing/src/framework/pytest_plugins/filler.py @@ -1,6 +1,5 @@ """Layer-agnostic pytest plugin for generating Ethereum test fixtures.""" -import functools import importlib import json import shutil @@ -13,13 +12,6 @@ import pytest -@functools.cache -def _spec_instance_for(fork_class: type) -> Any: - """Build the active fork's spec instance once and reuse across collection.""" - spec_class_method: Any = fork_class.spec_class # ty: ignore[unresolved-attribute] - return spec_class_method()() - - class FixtureCollector: """Collects generated fixtures and writes them to disk.""" @@ -234,11 +226,6 @@ def pytest_configure(config: pytest.Config) -> None: "markers", "valid_at(fork): specifies at which fork a test case is valid", ) - config.addinivalue_line( - "markers", - "requires(*capabilities): only collect when the active fork " - "advertises every listed runtime-checkable Protocol", - ) # Get options output_dir = Path(config.getoption("--output")) @@ -326,14 +313,6 @@ def _check_markers_valid_for_fork( """Check if test markers indicate validity for the given fork. Shared logic for both collection-time and parametrization-time fork filtering. - - Composition rules: - - - Fork-range markers form an intersection across kinds and a union - within a kind. - - The exact-fork marker short-circuits to a single-fork match. - - The capability marker AND-composes on top of either branch — the - active fork must satisfy every listed capability Protocol. """ has_valid_from = False has_valid_until = False @@ -342,7 +321,6 @@ def _check_markers_valid_for_fork( valid_from_forks = [] valid_until_forks = [] valid_at_forks = [] - required_capabilities: list[type] = [] for marker in markers: if marker.name == "valid_from": @@ -363,21 +341,12 @@ def _check_markers_valid_for_fork( target_fork = get_fork_by_name(fork_name) if target_fork: valid_at_forks.append(target_fork) - elif marker.name == "requires": - required_capabilities.extend(marker.args) - - def _capability_check() -> bool: - """Active fork must structurally satisfy every required capability.""" - if not required_capabilities: - return True - spec = _spec_instance_for(fork_class) - return all(isinstance(spec, cap) for cap in required_capabilities) - if not (has_valid_from or has_valid_until or has_valid_at or required_capabilities): + if not (has_valid_from or has_valid_until or has_valid_at): return True if has_valid_at: - return fork_class in valid_at_forks and _capability_check() + return fork_class in valid_at_forks from_valid = True if has_valid_from: @@ -387,7 +356,7 @@ def _capability_check() -> bool: if has_valid_until: until_valid = any(fork_class <= until_fork for until_fork in valid_until_forks) - return from_valid and until_valid and _capability_check() + return from_valid and until_valid def _is_test_item_valid_for_fork( diff --git a/pyproject.toml b/pyproject.toml index 30f716da2..418181702 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,10 +111,7 @@ addopts = [ ] markers = [ "slow: marks tests as slow (deselect with '-m \"not slow\"')", - "valid_from: marks tests as valid from a specific fork version", "valid_until: marks tests as valid until a specific fork version", - "valid_at: marks tests as valid only at a specific fork version", - "requires: marks tests as requiring one or more fork capabilities", "interop: integration tests for multiple leanSpec nodes", "num_validators: number of validators for interop test cluster", ] diff --git a/src/lean_spec/forks/__init__.py b/src/lean_spec/forks/__init__.py index 9371cf72e..367f67c57 100644 --- a/src/lean_spec/forks/__init__.py +++ b/src/lean_spec/forks/__init__.py @@ -1,7 +1,5 @@ """Multi-fork dispatch layer for leanSpec consensus specification.""" -from . import capabilities -from .capabilities import SigScheme from .lstar.containers import ( AggregatedAttestation, AggregatedAttestations, @@ -48,7 +46,6 @@ "ForkRegistry", "LstarSpec", "LstarStore", - "SigScheme", "SignedAggregatedAttestation", "SignedAttestation", "SignedBlock", @@ -58,5 +55,4 @@ "Store", "Validator", "Validators", - "capabilities", ] diff --git a/src/lean_spec/forks/capabilities.py b/src/lean_spec/forks/capabilities.py deleted file mode 100644 index 8ca738ba6..000000000 --- a/src/lean_spec/forks/capabilities.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Optional structural capabilities a fork may advertise.""" - -from typing import ClassVar, Protocol, runtime_checkable - -from lean_spec.subspecs.xmss.interface import GeneralizedXmssScheme - - -@runtime_checkable -class SigScheme(Protocol): - """Fork advertising a generalized XMSS signature scheme. - - - The runtime check only verifies the attribute is present. - - The static type contract is enforced by the type checker. - """ - - sig_scheme: ClassVar[GeneralizedXmssScheme] diff --git a/src/lean_spec/forks/lstar/spec.py b/src/lean_spec/forks/lstar/spec.py index e74570986..65f5d8524 100644 --- a/src/lean_spec/forks/lstar/spec.py +++ b/src/lean_spec/forks/lstar/spec.py @@ -43,7 +43,7 @@ TypeTwoMultiSignature, ) from lean_spec.subspecs.xmss.containers import PublicKey -from lean_spec.subspecs.xmss.interface import TARGET_SIGNATURE_SCHEME, GeneralizedXmssScheme +from lean_spec.subspecs.xmss.interface import TARGET_SIGNATURE_SCHEME from lean_spec.types import ( ZERO_HASH, Boolean, @@ -72,9 +72,6 @@ class LstarSpec(ForkProtocol): previous: ClassVar[type[ForkProtocol] | None] = None - # Capabilities advertised by this fork. - sig_scheme: ClassVar[GeneralizedXmssScheme] = TARGET_SIGNATURE_SCHEME - state_class: type[State] = State block_class: type[Block] = Block block_body_class: type[BlockBody] = BlockBody @@ -874,7 +871,6 @@ def verify_signatures( The block envelope holds one SSZ-encoded Type-2 proof binding every body attestation plus the proposer's signature over the block root. - The signing scheme is read from this fork's capability. Args: signed_block: The signed block whose merged proof is checked. @@ -1109,7 +1105,7 @@ def on_gossip_attestation( This method: - 1. Verifies the XMSS signature using this fork's capability + 1. Verifies the XMSS signature 2. Stores the signature when the node is in aggregator mode Subnet filtering happens at the p2p subscription layer — only @@ -1148,7 +1144,7 @@ def on_gossip_attestation( ) public_key = key_state.validators[validator_id].get_attestation_pubkey() - assert self.sig_scheme.verify( + assert TARGET_SIGNATURE_SCHEME.verify( public_key, attestation_data.slot, hash_tree_root(attestation_data), signature ), "Signature verification failed" @@ -1251,8 +1247,6 @@ def on_block( 3. Processing attestations included in the block body (on-chain) 4. Updating the forkchoice head - Signatures are verified using this fork's capability. - Raises: AssertionError: If parent block/state not found in store. """ diff --git a/tests/consensus/lstar/test_capability_gating.py b/tests/consensus/lstar/test_capability_gating.py deleted file mode 100644 index 849fafd84..000000000 --- a/tests/consensus/lstar/test_capability_gating.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Smoke tests for the capability-requirement marker dispatch.""" - -from typing import ClassVar, Protocol, runtime_checkable - -import pytest -from consensus_testing import StateExpectation, StateTransitionTestFiller, generate_pre_state -from framework import requires_capability - -from lean_spec.forks import SigScheme -from lean_spec.types import Slot - -pytestmark = pytest.mark.valid_until("Lstar") - - -@runtime_checkable -class _AbsentCapability(Protocol): - """A capability no real fork advertises.""" - - never_an_attribute_on_any_real_fork: ClassVar[object] - - -@requires_capability(SigScheme) -def test_runs_when_fork_advertises_sigscheme( - state_transition_test: StateTransitionTestFiller, -) -> None: - """Lstar advertises the signature-scheme capability — this test runs.""" - state_transition_test( - pre=generate_pre_state(), - blocks=[], - post=StateExpectation(slot=Slot(0)), - ) - - -@requires_capability(_AbsentCapability) -def test_deselected_when_capability_absent( - state_transition_test: StateTransitionTestFiller, -) -> None: - """No fork advertises the absent capability — this test must be deselected.""" - raise AssertionError("this test was executed — capability-requirement deselection is broken") diff --git a/tests/lean_spec/forks/test_capabilities.py b/tests/lean_spec/forks/test_capabilities.py deleted file mode 100644 index 36f969be6..000000000 --- a/tests/lean_spec/forks/test_capabilities.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Unit tests for fork capabilities and the requirement-marker dispatch.""" - -from typing import Any, ClassVar, Protocol, runtime_checkable - -import pytest -from framework.forks import BaseFork -from framework.markers import requires_capability -from framework.pytest_plugins.filler import _check_markers_valid_for_fork - -from lean_spec.forks import LstarSpec, SigScheme -from lean_spec.forks.protocol import ForkProtocol, SpecBlockType, SpecStateType -from lean_spec.subspecs.xmss.interface import TARGET_SIGNATURE_SCHEME -from lean_spec.types import ValidatorIndex - - -class _NoSigSpec(ForkProtocol): - """Synthetic fork without the signature-scheme capability.""" - - NAME: ClassVar[str] = "no_sig" - VERSION: ClassVar[int] = LstarSpec.VERSION + 1 - GOSSIP_DIGEST: ClassVar[str] = "deadbeef" - previous: ClassVar[type[ForkProtocol] | None] = LstarSpec - - def upgrade_state(self, state: SpecStateType) -> SpecStateType: - """Identity migration.""" - return state - - def generate_genesis(self, genesis_time: Any, validators: Any) -> SpecStateType: - """Not exercised.""" - raise NotImplementedError - - def create_store( - self, - state: SpecStateType, - anchor_block: SpecBlockType, - validator_id: ValidatorIndex | None, - ) -> Any: - """Not exercised.""" - raise NotImplementedError - - -class _NoSigFork(BaseFork): - """Fork wrapper around the no-capability synthetic spec.""" - - @classmethod - def name(cls) -> str: - return "_NoSigFork" - - @classmethod - def spec_class(cls) -> type[_NoSigSpec]: - return _NoSigSpec - - -class _LstarLikeFork(BaseFork): - """Fork wrapper around the real Lstar spec, kept local to avoid pulling - the consensus filler bootstrap into the unit-test import graph.""" - - @classmethod - def name(cls) -> str: - return "_LstarLikeFork" - - @classmethod - def spec_class(cls) -> type[LstarSpec]: - return LstarSpec - - -def _fork_by_name_table(*forks: type[BaseFork]) -> Any: - """Build a name lookup over the given forks.""" - table = {fork.name(): fork for fork in forks} - return table.get - - -def _mark(name: str, *args: Any) -> Any: - """Build a real pytest Mark via the public MarkDecorator path.""" - return getattr(pytest.mark, name)(*args).mark - - -class TestSigSchemeCapability: - """A fork advertises the signature-scheme capability by binding the attribute.""" - - def test_lstar_advertises_sigscheme(self) -> None: - """The real fork passes the structural check.""" - assert isinstance(LstarSpec(), SigScheme) - - def test_lstar_sig_scheme_is_target_scheme(self) -> None: - """The bound scheme is the same singleton resolved at import time.""" - assert LstarSpec.sig_scheme is TARGET_SIGNATURE_SCHEME - - def test_fork_without_attribute_not_recognized(self) -> None: - """A fork lacking the attribute is structurally rejected.""" - assert not isinstance(_NoSigSpec(), SigScheme) - - -class TestRequiresCapabilityHelper: - """The helper rejects non-runtime-checkable arguments at call time.""" - - def test_accepts_runtime_checkable_protocol(self) -> None: - """A runtime-checkable Protocol produces a usable marker.""" - decorator = requires_capability(SigScheme) - assert decorator.mark.name == "requires" - assert decorator.mark.args == (SigScheme,) - - def test_accepts_multiple_capabilities(self) -> None: - """Capabilities round-trip into marker args in the order given.""" - - @runtime_checkable - class _Other(Protocol): - other_attr: ClassVar[object] - - decorator = requires_capability(SigScheme, _Other) - assert decorator.mark.args == (SigScheme, _Other) - - def test_rejects_non_runtime_checkable_protocol(self) -> None: - """A plain Protocol is rejected at call time.""" - - class _NotRuntimeCheckable(Protocol): - sig_scheme: ClassVar[object] - - with pytest.raises(TypeError, match="runtime_checkable"): - requires_capability(_NotRuntimeCheckable) - - def test_rejects_plain_class(self) -> None: - """A non-Protocol class is rejected too.""" - - class _PlainClass: - sig_scheme: ClassVar[object] = object() - - with pytest.raises(TypeError, match="runtime_checkable"): - requires_capability(_PlainClass) - - -class TestMarkerDispatch: - """The capability marker AND-composes with the fork-range markers.""" - - def test_no_markers_passes(self) -> None: - """An unmarked test runs on any fork.""" - assert _check_markers_valid_for_fork([], _LstarLikeFork, _fork_by_name_table()) - - def test_capability_present_passes(self) -> None: - """Capability advertised → test included.""" - markers = [requires_capability(SigScheme).mark] - assert _check_markers_valid_for_fork(markers, _LstarLikeFork, _fork_by_name_table()) - - def test_capability_absent_fails(self) -> None: - """Capability missing → test deselected.""" - markers = [requires_capability(SigScheme).mark] - assert not _check_markers_valid_for_fork(markers, _NoSigFork, _fork_by_name_table()) - - def test_composes_with_valid_until_and_passes(self) -> None: - """Fork-range and capability both satisfied → test included.""" - markers = [ - _mark("valid_until", _LstarLikeFork.name()), - requires_capability(SigScheme).mark, - ] - assert _check_markers_valid_for_fork( - markers, _LstarLikeFork, _fork_by_name_table(_LstarLikeFork) - ) - - def test_composes_with_valid_until_and_fails_on_capability(self) -> None: - """Fork-range passes but capability missing → deselected.""" - markers = [ - _mark("valid_until", _NoSigFork.name()), - requires_capability(SigScheme).mark, - ] - assert not _check_markers_valid_for_fork( - markers, _NoSigFork, _fork_by_name_table(_NoSigFork) - ) - - def test_valid_at_short_circuit_still_checks_capability(self) -> None: - """Exact-fork match still requires the capability.""" - markers = [ - _mark("valid_at", _NoSigFork.name()), - requires_capability(SigScheme).mark, - ] - assert not _check_markers_valid_for_fork( - markers, _NoSigFork, _fork_by_name_table(_NoSigFork) - ) - - def test_multiple_capability_markers_compose_with_and(self) -> None: - """Stacked capability markers fail the whole if any one fails.""" - - @runtime_checkable - class _Absent(Protocol): - never_an_attribute_on_any_real_fork: ClassVar[object] - - markers = [ - requires_capability(SigScheme).mark, - requires_capability(_Absent).mark, - ] - assert not _check_markers_valid_for_fork(markers, _LstarLikeFork, _fork_by_name_table()) - - def test_dispatcher_raises_on_non_runtime_checkable_protocol(self) -> None: - """The dispatcher's own guard rejects non-runtime-checkable Protocols.""" - - class _NotRuntimeCheckable(Protocol): - sig_scheme: ClassVar[object] - - # Bypass the helper's validation to exercise the dispatcher's guard. - markers = [pytest.mark.requires.with_args(_NotRuntimeCheckable).mark] - with pytest.raises(TypeError, match="runtime_checkable"): - _check_markers_valid_for_fork(markers, _LstarLikeFork, _fork_by_name_table())