Skip to content

Commit 19cf8ec

Browse files
tcoratgerclaude
andauthored
refactor(forks): nest forks/ under new spec/ subpackage (leanEthereum#788)
* refactor(forks): nest forks/ under new spec/ subpackage Group per-fork consensus rules under a dedicated lean_spec.spec namespace so the protocol specification has a clear home as more spec content (SSZ, chain, validator, etc.) potentially moves alongside it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(crypto): collect koalabear and poseidon under spec/crypto/ Move the two cryptographic primitive subspecs into the spec/ subpackage created in the previous commit: - subspecs/koalabear/field.py -> spec/crypto/koalabear.py - subspecs/poseidon1/{constants,permutation}.py -> spec/crypto/poseidon.py Each primitive collapses into a single module: koalabear loses its re-exporting __init__, and poseidon merges its round-constants table with the permutation engine. The Poseidon1 class is renamed to Poseidon (and Poseidon1Params -> PoseidonParams) since the file no longer carries the variant number; PARAMS_16/PARAMS_24 keep their names. All import sites in src/, tests/, and packages/testing/ are updated. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(crypto): collapse subspecs/ssz/ into spec/crypto/merkleization.py Merge the four-file SSZ hashing subspec into a single module that lives alongside the other primitives under spec/crypto/. The hash-tree-root dispatch, the binary-tree merkleizer, the length-mix helper, and the two chunk-width constants now all sit in one place; the cross-file imports between hash.py, merkleization.py, and constants.py disappear. All import sites in src/, tests/, and packages/testing/ are updated to import from spec.crypto.merkleization. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(crypto): collapse zero-hash cache into a single expression Replace the three-piece machinery (named max-depth constant, builder function, module-level call) with one itertools.accumulate that folds the recurrence h_{d+1} = sha256(h_d || h_d) over the all-zero seed. The cache now covers depth 64 unconditionally, so _zero_tree_root drops the past-cache fallback loop and becomes a direct table lookup. The two tests that exercised the fallback are deleted with the dead code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(crypto): bullet-format the zero-hash table and depth-mapping comments Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor: move xmss under spec/crypto, rename subspecs -> node Two related reorgs that finish carving out spec/ vs runtime/: - XMSS is a cryptographic primitive specification, structurally identical to the koalabear/poseidon/merkleization siblings, so move the entire subpackage to lean_spec.spec.crypto.xmss alongside them. - Everything left under subspecs/ is reference-node runtime (api, networking, storage, sync, validator, observability, metrics, plus the chain/genesis fuzzy middle ground and the Node orchestrator itself). Rename it to lean_spec.node so the package name reflects what it is. The old inner subspecs/node/ subpackage is dissolved in the same move: its node.py and anchor.py lift up by one level so the new layout exposes lean_spec.node.{Node, NodeConfig, anchor.*} directly instead of nesting them inside lean_spec.node.node.*. All import sites in src/, tests/, and packages/testing/ are updated, the per-file ruff ignore in pyproject.toml moves with xmss, and the xmss internal relative imports are repointed to the new neighbors. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(node): move snappy/ under node/ Snappy framing is a wire-format runtime concern used by gossip and reqresp, not a piece of the protocol specification. Group it with the other runtime services under lean_spec.node. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(ssz): carve SSZ primitives into spec/ssz/, lift base.py to lean_spec.base The 8 SSZ-primitive modules under lean_spec.types (bitfields, boolean, byte_arrays, collections, container, exceptions, ssz_base, uint) move to lean_spec.spec.ssz alongside the other protocol-spec subpackages. What stays under lean_spec.types is the domain layer: Slot, Checkpoint, ValidatorIndex, SubnetId, AggregationBits, the RLP helpers, and the participation bits — types built on top of SSZ rather than defining it. base.py (CamelModel, StrictBaseModel) is not SSZ-specific and is used by both SSZ models and non-SSZ Pydantic models. Moving it would create a spec/ssz/ssz_base.py → types.base → types/__init__.py → spec/ssz cycle, so it lifts one level up to lean_spec.base, sitting next to config.py and log.py as generic Pydantic infrastructure. All 135 SSZ-touching import sites are split where they mix SSZ and domain symbols, and 21 sites importing StrictBaseModel/CamelModel are repointed at lean_spec.base. No backward-compat re-exports. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(tests): mirror src/lean_spec/ layout under tests/ The unit test tree was lagging behind the recent source reorgs. Bring tests/lean_spec/ into 1:1 correspondence with src/lean_spec/: - tests/lean_spec/types/{test_bitfields,test_boolean,test_byte_arrays, test_collections,test_container,test_ssz_base,test_uint}.py -> tests/lean_spec/spec/ssz/ - tests/lean_spec/types/test_base.py -> tests/lean_spec/test_base.py - tests/lean_spec/forks/ -> tests/lean_spec/spec/forks/ - tests/lean_spec/subspecs/{api,chain,genesis,metrics,networking, observability,storage,sync,validator}/ -> tests/lean_spec/node/ - tests/lean_spec/subspecs/node/{test_anchor,test_node}.py dissolved up into tests/lean_spec/node/ - tests/lean_spec/subspecs/koalabear/test_field.py -> tests/lean_spec/spec/crypto/test_koalabear.py - tests/lean_spec/subspecs/poseidon1/test_permutation.py -> tests/lean_spec/spec/crypto/test_poseidon.py - tests/lean_spec/subspecs/ssz/{test_hash,test_merkleization}.py -> tests/lean_spec/spec/crypto/ - tests/lean_spec/subspecs/xmss/ -> tests/lean_spec/spec/crypto/xmss/ - tests/lean_spec/subspecs/containers/test_attestation_aggregation.py -> tests/lean_spec/spec/forks/lstar/ - tests/lean_spec/snappy/ -> tests/lean_spec/node/snappy/ - tests/consensus/{lstar,devnet}/poseidon1 -> .../poseidon - tests/lean_spec/subspecs/conftest.py -> tests/lean_spec/node/conftest.py Justfile paths updated: codespell --skip for the snappy testdata fixture, and the test-consensus recipe now points at the new layout. Side fix: the node/__init__.py eager re-export of Node/NodeConfig was turning any import of lean_spec.node.<anything> into a load of the whole api -> networking -> reqresp.handler chain, which then tries to import SignedBlock from lean_spec.spec.forks while spec.forks is mid-init. The three call sites switch to the explicit module path lean_spec.node.node, and the __init__.py drops the re-export. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d866322 commit 19cf8ec

339 files changed

Lines changed: 1817 additions & 1946 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/rules/ssz-patterns.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
paths:
3-
- "src/lean_spec/forks/*/containers/**/*.py"
3+
- "src/lean_spec/spec/forks/*/containers/**/*.py"
44
- "src/lean_spec/types/**/*.py"
55
---
66

@@ -36,7 +36,7 @@ When creating SSZ types, follow these established patterns:
3636
Containers should be organized into modules with clear separation:
3737

3838
```
39-
src/lean_spec/forks/<fork>/containers/
39+
src/lean_spec/spec/forks/<fork>/containers/
4040
├── state/
4141
│ ├── __init__.py # Exports State and related types
4242
│ ├── state.py # Main State container class

Justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ typecheck *args:
4040
# Spell check source, tests, packages, and docs
4141
[group('quality')]
4242
spellcheck *args:
43-
uv run --group lint codespell src tests packages docs README.md CLAUDE.md --skip="*.lock,*.svg,.git,__pycache__,.pytest_cache,tests/lean_spec/snappy/testdata" --ignore-words=.codespell-ignore-words.txt "$@"
43+
uv run --group lint codespell src tests packages docs README.md CLAUDE.md --skip="*.lock,*.svg,.git,__pycache__,.pytest_cache,tests/lean_spec/node/snappy/testdata" --ignore-words=.codespell-ignore-words.txt "$@"
4444

4545
# Verify markdown formatting in docs/
4646
[group('quality')]
@@ -78,7 +78,7 @@ test-cov-gate *args:
7878
# Run consensus-only unit tests (containers, forkchoice, networking)
7979
[group('tests')]
8080
test-consensus *args:
81-
uv run --group test pytest -n auto --maxprocesses=10 --durations=10 --dist=worksteal tests/lean_spec/subspecs/containers tests/lean_spec/subspecs/forkchoice tests/lean_spec/subspecs/networking "$@"
81+
uv run --group test pytest -n auto --maxprocesses=10 --durations=10 --dist=worksteal tests/lean_spec/spec/forks tests/lean_spec/node/networking "$@"
8282

8383
# Canonical CI fixture run; contributors should use `uv run fill` directly.
8484
[group('tests'), private]

packages/testing/src/consensus_testing/forks/forks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from framework.forks import BaseFork
44

5-
from lean_spec.forks.lstar.spec import LstarSpec
5+
from lean_spec.spec.forks.lstar.spec import LstarSpec
66

77

88
class Lstar(BaseFork):

packages/testing/src/consensus_testing/genesis.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"""Consensus layer pre-state generation."""
22

3-
from lean_spec.forks.lstar.containers import (
3+
from lean_spec.spec.crypto.merkleization import hash_tree_root
4+
from lean_spec.spec.forks.lstar.containers import (
45
AggregatedAttestations,
56
Block,
67
BlockBody,
78
State,
89
Validator,
910
Validators,
1011
)
11-
from lean_spec.forks.lstar.spec import LstarSpec
12-
from lean_spec.subspecs.ssz.hash import hash_tree_root
13-
from lean_spec.types import Bytes52, Slot, Uint64, ValidatorIndex
12+
from lean_spec.spec.forks.lstar.spec import LstarSpec
13+
from lean_spec.spec.ssz import Bytes52, Uint64
14+
from lean_spec.types import Slot, ValidatorIndex
1415

1516
from .keys import XmssKeyManager
1617

packages/testing/src/consensus_testing/keys.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,34 +41,30 @@
4141
from typing import ClassVar, Literal
4242

4343
from lean_spec.config import LEAN_ENV
44-
from lean_spec.forks.lstar.containers import AggregatedAttestations, AttestationData
45-
from lean_spec.subspecs.koalabear import Fp
46-
from lean_spec.subspecs.ssz.hash import hash_tree_root
47-
from lean_spec.subspecs.xmss.aggregation import TypeOneMultiSignature
48-
from lean_spec.subspecs.xmss.constants import TARGET_CONFIG
49-
from lean_spec.subspecs.xmss.containers import (
44+
from lean_spec.spec.crypto.koalabear import Fp
45+
from lean_spec.spec.crypto.merkleization import hash_tree_root
46+
from lean_spec.spec.crypto.xmss.aggregation import TypeOneMultiSignature
47+
from lean_spec.spec.crypto.xmss.constants import TARGET_CONFIG
48+
from lean_spec.spec.crypto.xmss.containers import (
5049
PublicKey,
5150
SecretKey,
5251
Signature,
5352
ValidatorKeyPair,
5453
)
55-
from lean_spec.subspecs.xmss.interface import (
54+
from lean_spec.spec.crypto.xmss.interface import (
5655
PROD_SIGNATURE_SCHEME,
5756
TEST_SIGNATURE_SCHEME,
5857
GeneralizedXmssScheme,
5958
)
60-
from lean_spec.subspecs.xmss.types import (
59+
from lean_spec.spec.crypto.xmss.types import (
6160
HashDigestList,
6261
HashDigestVector,
6362
HashTreeOpening,
6463
Randomness,
6564
)
66-
from lean_spec.types import (
67-
Bytes32,
68-
Slot,
69-
Uint64,
70-
ValidatorIndex,
71-
)
65+
from lean_spec.spec.forks.lstar.containers import AggregatedAttestations, AttestationData
66+
from lean_spec.spec.ssz import Bytes32, Uint64
67+
from lean_spec.types import Slot, ValidatorIndex
7268

7369
KeyRole = Literal["attestation", "proposal"]
7470
"""Discriminator for which signing role's key to load from a validator key pair."""

packages/testing/src/consensus_testing/test_fixtures/api_endpoint.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
from collections.abc import Callable
44
from typing import Any, ClassVar
55

6-
from lean_spec.forks.lstar import Store
7-
from lean_spec.forks.lstar.containers import AggregatedAttestations, Block, BlockBody, State
8-
from lean_spec.forks.lstar.spec import LstarSpec
9-
from lean_spec.subspecs.ssz.hash import hash_tree_root
10-
from lean_spec.types import Bytes32, Slot, Uint64, ValidatorIndex
6+
from lean_spec.spec.crypto.merkleization import hash_tree_root
7+
from lean_spec.spec.forks.lstar import Store
8+
from lean_spec.spec.forks.lstar.containers import AggregatedAttestations, Block, BlockBody, State
9+
from lean_spec.spec.forks.lstar.spec import LstarSpec
10+
from lean_spec.spec.ssz import Bytes32, Uint64
11+
from lean_spec.types import Slot, ValidatorIndex
1112

1213
from ..genesis import build_anchor, generate_pre_state
1314
from .base import BaseConsensusFixture
@@ -150,7 +151,7 @@ def _metrics_response(_store: Store, _fixture: "ApiEndpointTest") -> dict[str, A
150151
so the fixture pins only the stable contract: status, content-type,
151152
and the full list of metric names clients must expose.
152153
"""
153-
from lean_spec.subspecs.metrics.registry import registry as metrics_registry
154+
from lean_spec.node.metrics.registry import registry as metrics_registry
154155

155156
# Names enumerated from the leanMetrics spec. Any change to this list
156157
# is a cross-client-visible metrics surface change and should be

packages/testing/src/consensus_testing/test_fixtures/fork_choice.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@
1111

1212
from pydantic import Field, model_validator
1313

14-
from lean_spec.forks.lstar.containers import (
14+
from lean_spec.node.chain.clock import Interval
15+
from lean_spec.spec.crypto.merkleization import hash_tree_root
16+
from lean_spec.spec.forks.lstar.containers import (
1517
AggregatedAttestations,
1618
Block,
1719
BlockBody,
1820
State,
1921
Validators,
2022
)
21-
from lean_spec.forks.lstar.spec import LstarSpec
22-
from lean_spec.subspecs.chain.clock import Interval
23-
from lean_spec.subspecs.ssz import hash_tree_root
24-
from lean_spec.types import Slot, Uint64, ValidatorIndex
23+
from lean_spec.spec.forks.lstar.spec import LstarSpec
24+
from lean_spec.spec.ssz import Uint64
25+
from lean_spec.types import Slot, ValidatorIndex
2526

2627
from ..keys import (
2728
XmssKeyManager,

packages/testing/src/consensus_testing/test_fixtures/gossipsub_handler.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
from typing import Any, ClassVar
1414
from unittest.mock import patch
1515

16-
from lean_spec.subspecs.networking import PeerId
17-
from lean_spec.subspecs.networking.gossipsub.behavior import GossipsubBehavior, PeerState
18-
from lean_spec.subspecs.networking.gossipsub.message import GossipsubMessage
19-
from lean_spec.subspecs.networking.gossipsub.parameters import GossipsubParameters
20-
from lean_spec.subspecs.networking.gossipsub.rpc import (
16+
from lean_spec.node.networking import PeerId
17+
from lean_spec.node.networking.gossipsub.behavior import GossipsubBehavior, PeerState
18+
from lean_spec.node.networking.gossipsub.message import GossipsubMessage
19+
from lean_spec.node.networking.gossipsub.parameters import GossipsubParameters
20+
from lean_spec.node.networking.gossipsub.rpc import (
2121
RPC,
2222
ControlGraft,
2323
ControlIDontWant,
@@ -27,7 +27,7 @@
2727
ControlPrune,
2828
Message,
2929
)
30-
from lean_spec.subspecs.networking.gossipsub.types import MessageId, Timestamp, TopicId
30+
from lean_spec.node.networking.gossipsub.types import MessageId, Timestamp, TopicId
3131

3232
from .base import BaseConsensusFixture
3333

packages/testing/src/consensus_testing/test_fixtures/networking_codec.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
from typing import Any, ClassVar
44

5-
from lean_spec.snappy import compress, decompress, frame_compress, frame_decompress
6-
from lean_spec.subspecs.networking.enr.enr import ENR
7-
from lean_spec.subspecs.networking.gossipsub.message import GossipsubMessage
8-
from lean_spec.subspecs.networking.gossipsub.rpc import (
5+
from lean_spec.node.networking.enr.enr import ENR
6+
from lean_spec.node.networking.gossipsub.message import GossipsubMessage
7+
from lean_spec.node.networking.gossipsub.rpc import (
98
RPC,
109
ControlGraft,
1110
ControlIDontWant,
@@ -16,15 +15,16 @@
1615
Message,
1716
SubOpts,
1817
)
19-
from lean_spec.subspecs.networking.gossipsub.topic import GossipTopic, TopicKind
20-
from lean_spec.subspecs.networking.gossipsub.types import TopicId
21-
from lean_spec.subspecs.networking.reqresp.codec import (
18+
from lean_spec.node.networking.gossipsub.topic import GossipTopic, TopicKind
19+
from lean_spec.node.networking.gossipsub.types import TopicId
20+
from lean_spec.node.networking.reqresp.codec import (
2221
ResponseCode,
2322
decode_request,
2423
encode_request,
2524
)
26-
from lean_spec.subspecs.networking.transport.peer_id import KeyType, PeerId, PublicKeyProto
27-
from lean_spec.subspecs.networking.varint import decode_varint, encode_varint
25+
from lean_spec.node.networking.transport.peer_id import KeyType, PeerId, PublicKeyProto
26+
from lean_spec.node.networking.varint import decode_varint, encode_varint
27+
from lean_spec.node.snappy import compress, decompress, frame_compress, frame_decompress
2828
from lean_spec.types import SubnetId
2929

3030
from .base import BaseConsensusFixture

packages/testing/src/consensus_testing/test_fixtures/poseidon_permutation.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
1-
"""Poseidon1 permutation test fixture.
1+
"""Poseidon permutation test fixture.
22
3-
Generates JSON test vectors for the Poseidon1 permutation over the
3+
Generates JSON test vectors for the Poseidon permutation over the
44
KoalaBear field at widths 16 and 24. Clients must produce identical
55
output states bit-for-bit for every input state.
66
"""
77

88
from typing import Any, ClassVar
99

10-
from lean_spec.subspecs.koalabear.field import Fp
11-
from lean_spec.subspecs.poseidon1 import PARAMS_16, PARAMS_24, Poseidon1
10+
from lean_spec.spec.crypto.koalabear import Fp
11+
from lean_spec.spec.crypto.poseidon import PARAMS_16, PARAMS_24, Poseidon
1212

1313
from .base import BaseConsensusFixture
1414

1515

1616
class PoseidonPermutationTest(BaseConsensusFixture):
17-
"""Fixture for Poseidon1 permutation conformance.
17+
"""Fixture for Poseidon permutation conformance.
1818
1919
Each vector names the permutation width and supplies an input state
2020
as decimal strings. The fixture runs the spec's permutation engine
@@ -24,7 +24,7 @@ class PoseidonPermutationTest(BaseConsensusFixture):
2424
"""
2525

2626
format_name: ClassVar[str] = "poseidon_permutation"
27-
description: ClassVar[str] = "Tests Poseidon1 permutation at widths 16 and 24"
27+
description: ClassVar[str] = "Tests Poseidon permutation at widths 16 and 24"
2828

2929
width: int
3030
"""State width. Must be 16 or 24."""
@@ -36,7 +36,7 @@ class PoseidonPermutationTest(BaseConsensusFixture):
3636
"""Computed output state. Filled by make_fixture."""
3737

3838
def make_fixture(self) -> "PoseidonPermutationTest":
39-
"""Run the Poseidon1 permutation and produce the output state.
39+
"""Run the Poseidon permutation and produce the output state.
4040
4141
Returns:
4242
A copy of this fixture with output populated.
@@ -45,11 +45,11 @@ def make_fixture(self) -> "PoseidonPermutationTest":
4545
ValueError: If the width is unsupported.
4646
"""
4747
if self.width == 16:
48-
engine = Poseidon1(PARAMS_16)
48+
engine = Poseidon(PARAMS_16)
4949
elif self.width == 24:
50-
engine = Poseidon1(PARAMS_24)
50+
engine = Poseidon(PARAMS_24)
5151
else:
52-
raise ValueError(f"Unsupported Poseidon1 width: {self.width}")
52+
raise ValueError(f"Unsupported Poseidon width: {self.width}")
5353

5454
state_ints = [int(x) for x in self.input["inputState"]]
5555
if len(state_ints) != self.width:

0 commit comments

Comments
 (0)