Skip to content

refactor(forks): move verify_signatures body into the spec (Stage 4C, part 1 of #686)#704

Merged
tcoratger merged 9 commits intoleanEthereum:mainfrom
tcoratger:refactor/move-bodies-stage4c
May 3, 2026
Merged

refactor(forks): move verify_signatures body into the spec (Stage 4C, part 1 of #686)#704
tcoratger merged 9 commits intoleanEthereum:mainfrom
tcoratger:refactor/move-bodies-stage4c

Conversation

@tcoratger
Copy link
Copy Markdown
Collaborator

Summary

First piece of Stage 4C of the multi-fork architecture refactor (#686): move container method bodies into the spec class, replace literal class references with `self._class(...)`. This PR moves the smallest, most isolated piece — `SignedBlock.verify_signatures` — to establish the pattern.

What changed

  • The XMSS signature verification body now lives on `LstarSpec.verify_signatures`. `SignedBlock` is a pure SSZ data container.
  • The forkchoice store's `on_block` reaches the verifier via a deferred `LstarSpec()` import — necessary until `Store.on_block` itself moves to the spec.

What's left for Stage 4C

The bigger pieces — `State` methods (`state_transition`, `process_`, `build_block`) and the entire `Store` surface (`on_block`, `on_tick`, `on_gossip_`, `produce_*`, plus internal helpers like `update_head`, `aggregate`, `tick_interval`) — follow in subsequent PRs. Each of those moves involves ~hundreds of lines of bodies plus the same `self._class` substitution.

Test plan

  • `uvx tox -e all-checks` (ruff + format + ty + codespell + mdformat) green.

🤖 Generated with Claude Code

tcoratger and others added 9 commits May 3, 2026 16:30
Moves the full XMSS signature verification logic from
SignedBlock.verify_signatures into LstarSpec.verify_signatures.
SignedBlock becomes a pure SSZ data container.

Internal callers that still hold a Store (notably Store.on_block,
which itself moves to the spec class in a follow-up) reach the
verification path via a deferred import of LstarSpec to sidestep
the spec ↔ store module-load cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates every State and Store method body into LstarSpec. Containers
(State, Store, SignedBlock) become thin Pydantic data classes whose
methods are one-line forwarders to the active fork spec, reached via a
deferred import that breaks the spec ↔ container module-load cycle.

Inside the moved bodies, every literal Block(...), BlockBody(...),
BlockHeader(...), Config(...), AggregatedAttestations(...), and other
container constructor is now self.<name>_class(...). An inheriting fork
that swaps a single container type therefore receives the parent fork's
logic for free.

Observability hooks (observe_state_transition, observe_on_block,
observe_on_attestation) ride along with the bodies, preserving the
metrics surface.

The obsolete delegator-forwarding test file is removed; behavioural
coverage now lives in the existing state-transition, fork-choice, and
block-production test suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eanEthereum#686)

State, Store, and SignedBlock become pure Pydantic data containers.
All forwarder methods that delegated through the lazy spec singleton
are removed; the lazy singleton helpers are removed alongside them.

Every remaining call site that used to go through a container method
now goes through the active fork spec:

- Tests use the session-scoped `spec` fixture from `tests/lean_spec/conftest.py`.
- Subspec services (`sync`, `validator`, `chain`, plus the fork-choice
  API endpoint) carry a module-level `_SPEC = LstarSpec()` constant.
- Library helpers (`tests/lean_spec/helpers/builders.py`,
  `packages/testing/...`) follow the same `_SPEC` pattern.

`ForkProtocol.generate_genesis` and `ForkProtocol.create_store` become
abstract; the previous default implementations referenced container
methods that no longer exist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Store.blocks field is annotated as `BlockLookup`, which was defined
as `dict[Bytes32, "Block"]` — a string forward reference. After the
container methods moved off Store, Pydantic's model rebuild could no
longer resolve `Block` because it was never imported into store.py
alongside the alias.

Drop the forward-reference quoting: `BlockLookup` lives in the same
module as `Block`, so the type can refer to the class directly. Pydantic
then resolves `Store.blocks` correctly through the alias re-export.

Without this, every consensus filler that constructed a Store via
`spec.create_store(...)` raised
`PydanticUserError: 'Store' is not fully defined`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The forwarders that used to live on `Store` (and were patched in
`tests/lean_spec/subspecs/{sync,chain,networking,validator}/`) are gone
after Stage 4D. The mocks (`MockStore`, `MockForkchoiceStore`) still
implement the same method surface, but the service code now calls the
real spec, which expects a Pydantic Store.

Add an autouse fixture per affected subspec that patches the active
spec's methods to delegate back to `store.method(...)`. The mocks
intercept calls in-place, preserving every test's recording semantics
without touching service code.

The validator service tests that previously patched
`Store.produce_block_with_signatures` and `Store.on_gossip_attestation`
now patch the matching methods on the validator service's `_SPEC` —
same intent, current attribute path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Casts were a workaround for Liskov violations on LstarSpec.create_store
when it returned the SpecStoreType protocol while concretely producing
Store. cast had no runtime effect and pushed type-checker noise into
fixtures and tests.

Move the imprecision into the fork itself: create_store now declares
its concrete Store return and suppresses the override warning at the
single definition site. Callers receive Store directly and need no cast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each module declared its own _SPEC = LstarSpec(), creating value-equal
but identity-distinct instances. The pattern also baked LstarSpec into
17 import sites; a future fork would have to grep-and-replace them all.

Production services (chain, sync, validator, api) now take spec as a
dataclass field with default_factory=LstarSpec — explicit at the
composition root in node.py, optional in tests. node.py narrows
config.fork (ForkProtocol) to LstarSpec once with isinstance, which
also lets the cast(State, ...) and cast(Store, ...) at the genesis
construction sites drop.

Test conftests that intercept spec calls now monkey-patch LstarSpec
at the class level (not the deleted module-level _SPEC instance).
Test types and fixtures instantiate LstarSpec at call time — no
module-level cache, no shared mutable state to alias.

ForkProtocol still declares only the three abstract construction
methods (generate_genesis, create_store, upgrade_state). Services and
tests that drive consensus methods (process_slots, build_block,
tick_interval, ...) keep the concrete LstarSpec type until the
protocol surface is widened in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three autouse fixtures in chain/sync/networking conftests patched
LstarSpec class methods so MockStore / MockForkchoiceStore could
intercept consensus calls in place. Class-level patching mutates
shared state and runs against every test in the directory whether
needed or not.

Now that services accept a spec field, tests inject a small
StoreInterceptingSpec subclass that forwards each spec call back to
the store argument. make_store() (used by sync/networking tests)
hands the intercepting spec to the real SyncService transparently.
Chain test_service.py threads it through ChainService directly.

Two conftests delete entirely; the sync conftest keeps only its
sample_checkpoint / sample_status fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two validator tests still resolved `lean_spec.subspecs.validator.service._SPEC`,
which was removed when the spec moved onto the service as a field. Patching now
targets `service.spec` directly via `patch.object`, which also exercises the
single instance the test actually calls into.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tcoratger tcoratger merged commit 45ba21e into leanEthereum:main May 3, 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.

1 participant