feat(forks): add SigScheme capability and @requires marker (Stage 7 of #686)#715
Merged
tcoratger merged 3 commits intoMay 11, 2026
Merged
Conversation
…leanEthereum#686) Introduces the first fork-level capability and a pytest marker to gate tests on capability presence. Capability ---------- - New `SigScheme` runtime-checkable Protocol in `forks/capabilities.py` asserts a `sig_scheme: ClassVar[GeneralizedXmssScheme]` attribute. - `LstarSpec` binds `sig_scheme = TARGET_SIGNATURE_SCHEME` so `isinstance(LstarSpec(), SigScheme)` returns True. - The three spec methods that previously took `scheme=TARGET_SIGNATURE_SCHEME` (`verify_signatures`, `on_gossip_attestation`, `on_block`) drop the parameter and read `self.sig_scheme` directly. The capability becomes the runtime source of truth. Marker ------ - `requires(*capabilities)` pytest marker, registered in `pytest_plugins/filler.py`. Composes (AND) with the existing `valid_from` / `valid_until` / `valid_at` fork-range markers. - `_check_markers_valid_for_fork` instantiates the active spec once and runs `isinstance(spec, capability)` per required capability. - A `requires(...)` helper in `framework.markers` works around pytest's auto-detect-class shortcut (which trips on Protocol args to `@pytest.mark.requires(...)`). Tests ----- - 11 unit tests in `tests/lean_spec/forks/test_capabilities.py` cover the Protocol and the dispatch helper (composition with the fork-range markers, multiple `@requires` markers, error path for non-runtime_checkable Protocol). - One smoke filler test in `tests/consensus/lstar/test_capability_gating.py` exercises the marker through pytest's live collection: one test marked with SigScheme runs, one marked with a synthetic absent capability is deselected. Filler scheme override ---------------------- The three filler call sites that previously passed `scheme=LEAN_ENV_TO_SCHEMES[self.lean_env]` to spec methods (in `test_fixtures/fork_choice.py` and `test_types/block_spec.py`) drop the kwarg. The PR description has the trade-off note and revert path if that override is in fact needed somewhere we missed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rename the marker helper to requires_capability and validate runtime-checkable Protocols at call time (fail at import, not at collection) - Cache the fork spec instance in marker dispatch instead of constructing it once per test - Re-export the capabilities namespace from lean_spec.forks so future capabilities don't need new import-site edits - Register valid_from / valid_at / requires markers in pyproject so unit tests can build real pytest Marks under strict-markers - Drop the hand-rolled Mark stand-in in tests; build real Marks via the MarkDecorator path; drop is True / is False on bool predicates - Tighten docstrings per project style (no paragraph blocks, no backtick references, no internal-name references) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tcoratger
approved these changes
May 11, 2026
Collaborator
tcoratger
left a comment
There was a problem hiding this comment.
I've just modified a bit of doc to compress a bit and some namings to avoid you further work but I didn't touch the logic, thanks a lot!
38 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 7 of #686 — adds the first fork-level capability (
SigScheme) and a@requires(capability)pytest marker that composes with the existingvalid_from/valid_until/valid_atmarkers.What changes
Capability
SigSchemeruntime-checkable Protocol insrc/lean_spec/forks/capabilities.pyasserts asig_scheme: ClassVar[GeneralizedXmssScheme]attribute.LstarSpecbindssig_scheme: ClassVar = TARGET_SIGNATURE_SCHEMEsoisinstance(LstarSpec(), SigScheme)returns True.scheme=TARGET_SIGNATURE_SCHEME(verify_signatures,on_gossip_attestation,on_block) drop the parameter and readself.sig_schemedirectly. The capability becomes the runtime source of truth.Marker
requires(*capabilities)pytest marker, registered inpackages/testing/src/framework/pytest_plugins/filler.py. Composes (AND) with the existing fork-range markers._check_markers_valid_for_forkinstantiates the active spec once and runsisinstance(spec, capability)per required capability.requires(...)helper inframework.markersworks around pytest's auto-detect-class shortcut (which trips on Protocol args to@pytest.mark.requires(...)). Users write@requires(SigScheme)orpytestmark = [..., requires(SigScheme)].Tests
tests/lean_spec/forks/test_capabilities.pycover the Protocol and the dispatch helper (composition with the fork-range markers, multiple@requiresmarkers, error path for non-runtime_checkable Protocol).tests/consensus/lstar/test_capability_gating.pyexercises the marker through pytest's live collection: one test marked withSigSchemeruns, one marked with a synthetic absent capability is deselected.Note on dropping the filler scheme override — easy to revert if we missed a case
The three filler call sites in
test_fixtures/fork_choice.pyandtest_types/block_spec.pypreviously passedscheme=LEAN_ENV_TO_SCHEMES[self.lean_env]tospec.on_blockandspec.on_gossip_attestation. While designing the capability we traced the chain and concluded the override was a no-op:src/lean_spec/subspecs/xmss/interface.py:568resolvesTARGET_SIGNATURE_SCHEMEfromLEAN_ENVat module import.packages/testing/src/framework/test_fixtures/base.py:43declareslean_env: str = Field(default=LEAN_ENV).fixture.lean_envto a value different from the env-var-derived default. The onlylean_env=override across the tree is in the keygen CLI tool (consensus_testing/keys.py:774), which generates key files — not fixture instances.So at runtime
LEAN_ENV_TO_SCHEMES[fixture.lean_env]always equaledTARGET_SIGNATURE_SCHEMEalways equaledLstarSpec.sig_scheme. The override and the default were three names for the same value, and dropping it makes the capability the visible source of truth.If we missed a case where the env-var-vs-fixture-field divergence is actually used (e.g. some tooling generates prod-scheme vectors from a test-env process), reverting just the override is a small surgical change:
scheme: GeneralizedXmssScheme | None = Noneto the three method signatures inLstarSpec.scheme = scheme if scheme is not None else self.sig_schemeat the top of each body.scheme=LEAN_ENV_TO_SCHEMES[self.lean_env]on the three filler call sites infork_choice.py:336,354andblock_spec.py:451.The capability and
@requiresmarker scaffolding is independent of that decision and stays either way.Out of scope
NetworkCapable, etc.). Stage 7 says "one real capability protocol first." A future PR introduces more as real divergence demands them.lean_env: strfield onBaseConsensusFixtureitself.Test plan
just check— green (ruff, format, ty, codespell, mdformat)uv run pytest --no-cov— 3313 passeduv run fill --fork=Lstar tests/consensus/lstar/test_capability_gating.py— 1 passed, 1 deselected (the deselected test wouldraise AssertionErrorif it ran, so passing proves the marker plumbing works end-to-end)uv run fill --fork=Lstaron a sample of existing fillers (test_genesis.py,test_fork_choice_head.py,test_valid_signatures.py) — 20 passed, confirming no regression from dropping thescheme=kwarg🤖 Generated with Claude Code