Skip to content

feat(forks): add SigScheme capability and @requires marker (Stage 7 of #686)#715

Merged
tcoratger merged 3 commits into
leanEthereum:mainfrom
leolara:multi-fork-stage7-sigscheme
May 11, 2026
Merged

feat(forks): add SigScheme capability and @requires marker (Stage 7 of #686)#715
tcoratger merged 3 commits into
leanEthereum:mainfrom
leolara:multi-fork-stage7-sigscheme

Conversation

@leolara
Copy link
Copy Markdown
Contributor

@leolara leolara commented May 11, 2026

Stage 7 of #686 — adds the first fork-level capability (SigScheme) and a @requires(capability) pytest marker that composes with the existing valid_from / valid_until / valid_at markers.

What changes

Capability

  • New SigScheme runtime-checkable Protocol in src/lean_spec/forks/capabilities.py asserts a sig_scheme: ClassVar[GeneralizedXmssScheme] attribute.
  • LstarSpec binds sig_scheme: ClassVar = 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 packages/testing/src/framework/pytest_plugins/filler.py. Composes (AND) with the existing 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(...)). Users write @requires(SigScheme) or pytestmark = [..., requires(SigScheme)].

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.

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.py and test_types/block_spec.py previously passed scheme=LEAN_ENV_TO_SCHEMES[self.lean_env] to spec.on_block and spec.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:568 resolves TARGET_SIGNATURE_SCHEME from LEAN_ENV at module import.
  • packages/testing/src/framework/test_fixtures/base.py:43 declares lean_env: str = Field(default=LEAN_ENV).
  • No fixture construction site anywhere in the codebase sets fixture.lean_env to a value different from the env-var-derived default. The only lean_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 equaled TARGET_SIGNATURE_SCHEME always equaled LstarSpec.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:

  1. Re-add scheme: GeneralizedXmssScheme | None = None to the three method signatures in LstarSpec.
  2. Re-add scheme = scheme if scheme is not None else self.sig_scheme at the top of each body.
  3. Re-instate scheme=LEAN_ENV_TO_SCHEMES[self.lean_env] on the three filler call sites in fork_choice.py:336,354 and block_spec.py:451.

The capability and @requires marker scaffolding is independent of that decision and stays either way.

Out of scope

  • Defining a second capability (NetworkCapable, etc.). Stage 7 says "one real capability protocol first." A future PR introduces more as real divergence demands them.
  • Removing the vestigial lean_env: str field on BaseConsensusFixture itself.

Test plan

  • just check — green (ruff, format, ty, codespell, mdformat)
  • uv run pytest --no-cov — 3313 passed
  • uv run fill --fork=Lstar tests/consensus/lstar/test_capability_gating.py — 1 passed, 1 deselected (the deselected test would raise AssertionError if it ran, so passing proves the marker plumbing works end-to-end)
  • uv run fill --fork=Lstar on a sample of existing fillers (test_genesis.py, test_fork_choice_head.py, test_valid_signatures.py) — 20 passed, confirming no regression from dropping the scheme= kwarg

🤖 Generated with Claude Code

leolara and others added 3 commits May 11, 2026 22:31
…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>
Copy link
Copy Markdown
Collaborator

@tcoratger tcoratger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@tcoratger tcoratger merged commit 98799f0 into leanEthereum:main May 11, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants