refactor(forks): move verify_signatures body into the spec (Stage 4C, part 1 of #686)#704
Merged
tcoratger merged 9 commits intoleanEthereum:mainfrom May 3, 2026
Conversation
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>
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.
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
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
🤖 Generated with Claude Code