diff --git a/packages/testing/src/consensus_testing/forks/forks.py b/packages/testing/src/consensus_testing/forks/forks.py index bfb6b2fd6..21677c383 100644 --- a/packages/testing/src/consensus_testing/forks/forks.py +++ b/packages/testing/src/consensus_testing/forks/forks.py @@ -2,8 +2,6 @@ from framework.forks import BaseFork -from lean_spec.spec.forks.lstar.spec import LstarSpec - class Lstar(BaseFork): """Lstar fork — base fork for the lean Ethereum protocol.""" @@ -12,8 +10,3 @@ class Lstar(BaseFork): def name(cls) -> str: """Return the fork name.""" return "Lstar" - - @classmethod - def spec_class(cls) -> type[LstarSpec]: - """Return the ForkProtocol implementation for this fork.""" - return LstarSpec diff --git a/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py b/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py index f90a88ade..f3c448785 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py +++ b/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py @@ -11,6 +11,7 @@ from pydantic import Field, model_validator +from consensus_testing.genesis import generate_pre_state from consensus_testing.keys import XmssKeyManager from consensus_testing.test_fixtures.base import BaseConsensusFixture from consensus_testing.test_types import ( @@ -56,12 +57,12 @@ class ForkChoiceTest(BaseConsensusFixture): description: ClassVar[str] = "Tests event-driven fork choice through Store operations" """Human-readable summary for fixture documentation.""" - anchor_state: State | None = None + anchor_state: State = Field(default_factory=generate_pre_state) """ Initial trusted consensus state. - Most tests start from genesis. - The pytest fixture provides this automatically. + Defaults to the standard genesis state. + Spell it out only when the test needs a non-default anchor. """ anchor_block: Block | None = None @@ -120,7 +121,7 @@ def set_anchor_block_default(self) -> Self: Most tests start from genesis. Deriving the anchor block from state reduces boilerplate. """ - if self.anchor_block is None and self.anchor_state is not None: + if self.anchor_block is None: # Build a minimal genesis block from the state's header fields. # # The state already contains the block header. @@ -177,7 +178,6 @@ def make_fixture(self) -> Self: # # Pydantic validators should have populated these fields. # These assertions guard against misuse. - assert self.anchor_state is not None, "anchor state must be set before making fixture" assert self.anchor_block is not None, "anchor block must be set before making fixture" assert self.max_slot is not None, "max slot must be set before making fixture" diff --git a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py index d41d7c7d1..d14ce4b48 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -2,8 +2,9 @@ from typing import Any, ClassVar -from pydantic import ConfigDict, PrivateAttr, field_serializer +from pydantic import ConfigDict, Field, PrivateAttr, field_serializer +from consensus_testing.genesis import generate_pre_state from consensus_testing.keys import XmssKeyManager from consensus_testing.test_fixtures.base import BaseConsensusFixture from consensus_testing.test_types import AggregatedAttestationSpec, BlockSpec, StateExpectation @@ -50,8 +51,13 @@ class StateTransitionTest(BaseConsensusFixture): model_config = ConfigDict(arbitrary_types_allowed=True) - pre: State - """The initial consensus state before processing.""" + pre: State = Field(default_factory=generate_pre_state) + """ + The initial consensus state before processing. + + Defaults to the standard genesis state. + Spell it out only when the test needs a non-default pre-state. + """ blocks: list[BlockSpec] """ diff --git a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py index ce52c8e00..2e9b66fd6 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -6,6 +6,7 @@ from pydantic import Field +from consensus_testing.genesis import generate_pre_state from consensus_testing.keys import XmssKeyManager from consensus_testing.test_fixtures.base import BaseConsensusFixture from consensus_testing.test_types import BlockSpec @@ -39,11 +40,11 @@ class VerifySignaturesTest(BaseConsensusFixture): format_name: ClassVar[str] = "verify_signatures_test" description: ClassVar[str] = "Tests signature verification for signed blocks." - anchor_state: State | None = None + anchor_state: State = Field(default_factory=generate_pre_state) """ The initial consensus state before processing. - If not provided, the framework will use the genesis fixture. + Defaults to the standard genesis state. """ block: BlockSpec = Field(exclude=True) @@ -101,9 +102,6 @@ def make_fixture(self) -> VerifySignaturesTest: Raises: AssertionError: If signature verification fails unexpectedly. """ - # Ensure anchor_state is set - assert self.anchor_state is not None, "anchor state must be set before making the fixture" - # Use shared key manager key_manager = XmssKeyManager.shared() @@ -264,7 +262,6 @@ def _apply_tamper(self, signed_block: SignedBlock) -> SignedBlock: attestations = body.attestations.data if len(attestations) < 2: raise ValueError("swap_first_two_attestations requires at least two attestations") - assert self.anchor_state is not None key_manager = XmssKeyManager.shared() original_attestation_proofs = [ diff --git a/packages/testing/src/framework/pytest_plugins/filler.py b/packages/testing/src/framework/pytest_plugins/filler.py index f872e1133..315dea4d0 100644 --- a/packages/testing/src/framework/pytest_plugins/filler.py +++ b/packages/testing/src/framework/pytest_plugins/filler.py @@ -8,7 +8,6 @@ from typing import Any import pytest -from consensus_testing import generate_pre_state from consensus_testing.forks import FORKS_BY_NAME from consensus_testing.test_fixtures import ( ApiEndpointTest, @@ -341,22 +340,6 @@ def test_case_description(request: pytest.FixtureRequest) -> str: return combined_docstring -@pytest.fixture(scope="function") -def pre(request: pytest.FixtureRequest, fork: Any) -> Any: - """ - Default consensus pre-state. - - Tests can request this fixture to customize the initial state, - or omit it to use the default (auto-injected by framework). - """ - spec = fork.spec_class()() - - if hasattr(request, "param"): - return generate_pre_state(fork=spec, **request.param) - - return generate_pre_state(fork=spec) - - def base_spec_filler_parametrizer(fixture_class: Any) -> Any: """ Generate pytest.fixture for a given fixture class. @@ -376,7 +359,6 @@ def base_spec_filler_parametrizer_func( request: pytest.FixtureRequest, fork: Any, test_case_description: str, - pre: Any, # Auto-inject pre fixture ) -> Any: """Fixture used to instantiate an auto-fillable fixture object.""" @@ -384,15 +366,6 @@ class FixtureWrapper(fixture_class): """Wrapper class that auto-fills and collects fixtures on instantiation.""" def __init__(self, **kwargs: Any) -> None: - # Auto-inject pre-state if not provided by test - if "pre" not in kwargs and "anchor_state" not in kwargs: - # Determine which field to inject based on fixture type - if hasattr(fixture_class, "__annotations__"): - if "pre" in fixture_class.__annotations__: - kwargs["pre"] = pre - elif "anchor_state" in fixture_class.__annotations__: - kwargs["anchor_state"] = pre - super().__init__(**kwargs) filled_fixture = self.make_fixture() diff --git a/src/lean_spec/spec/crypto/xmss/__init__.py b/src/lean_spec/spec/crypto/xmss/__init__.py index aa19e1b20..4ecaedee3 100644 --- a/src/lean_spec/spec/crypto/xmss/__init__.py +++ b/src/lean_spec/spec/crypto/xmss/__init__.py @@ -10,6 +10,11 @@ from lean_multisig_py import setup_prover +# Why: break the import cycle between the signature and fork containers. +# The signature containers need the fork slot type, while the fork +# aggregation containers import the signature containers back. +# Loading the forks package first lets the cycle resolve from any entry point. +import lean_spec.spec.forks # noqa: F401 from lean_spec.config import LEAN_ENV from lean_spec.spec.crypto.xmss.containers import PublicKey, SecretKey from lean_spec.spec.crypto.xmss.interface import TARGET_SIGNATURE_SCHEME, GeneralizedXmssScheme diff --git a/tests/consensus/lstar/fc/test_block_attestation_limits.py b/tests/consensus/lstar/fc/test_block_attestation_limits.py index dde40984b..20eb5ee63 100644 --- a/tests/consensus/lstar/fc/test_block_attestation_limits.py +++ b/tests/consensus/lstar/fc/test_block_attestation_limits.py @@ -8,7 +8,6 @@ ForkChoiceStep, ForkChoiceTestFiller, StoreChecks, - generate_pre_state, ) from consensus_testing.keys import XmssKeyManager @@ -99,7 +98,6 @@ def test_block_with_maximum_attestations( ) fork_choice_test( - anchor_state=generate_pre_state(), steps=chain, ) @@ -175,6 +173,5 @@ def test_block_exceeding_maximum_attestations_is_rejected( ) fork_choice_test( - anchor_state=generate_pre_state(), steps=chain, ) diff --git a/tests/consensus/lstar/state_transition/test_block_processing.py b/tests/consensus/lstar/state_transition/test_block_processing.py index 9b848f318..64c49538c 100644 --- a/tests/consensus/lstar/state_transition/test_block_processing.py +++ b/tests/consensus/lstar/state_transition/test_block_processing.py @@ -37,7 +37,6 @@ def test_process_first_block_after_genesis( This is the foundation for all subsequent blocks. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1)), ], @@ -70,7 +69,6 @@ def test_linear_chain_multiple_blocks( 5. Final state at slot 5 """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -114,7 +112,6 @@ def test_blocks_with_gaps( This validates resilience to gaps. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(4), parent_label="block_1", label="block_4"), @@ -150,7 +147,6 @@ def test_block_at_large_slot_number( 4. State remains consistent """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(100)), ], @@ -182,7 +178,6 @@ def test_block_at_very_large_slot_with_many_skipped( 5. No integer overflow or memory issues """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(500)), ], @@ -225,7 +220,6 @@ def test_block_with_invalid_proposer( # - expected proposer is Uint64(1), # - we'll try to use Uint64(5) instead state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec( slot=Slot(1), @@ -263,7 +257,6 @@ def test_block_with_invalid_parent_root( Without this check, attackers could create invalid chain branches. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec( slot=Slot(1), @@ -299,7 +292,6 @@ def test_block_with_invalid_state_root( This is a critical validation - without it, proposers could claim any arbitrary state. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec( slot=Slot(1), @@ -377,7 +369,6 @@ def test_block_extends_deep_chain( ) state_transition_test( - pre=generate_pre_state(), blocks=blocks, post=StateExpectation(slot=Slot(20)), ) @@ -419,7 +410,6 @@ def test_empty_blocks( ] state_transition_test( - pre=generate_pre_state(), blocks=blocks, post=StateExpectation( slot=Slot(6), latest_block_header_slot=Slot(6), historical_block_hashes_count=6 @@ -451,7 +441,6 @@ def test_empty_blocks_with_missed_slots( """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), body=None, parent_label="block_1", label="block_2"), diff --git a/tests/consensus/lstar/state_transition/test_finalization.py b/tests/consensus/lstar/state_transition/test_finalization.py index 42e13c707..7d851128a 100644 --- a/tests/consensus/lstar/state_transition/test_finalization.py +++ b/tests/consensus/lstar/state_transition/test_finalization.py @@ -45,7 +45,6 @@ def test_finalization_on_next_justifiable_step( 7. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -120,7 +119,6 @@ def test_pending_justification_survives_finalization_rebase( 8. justifications_validators contains a single 1-of-4 pending tally """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -307,7 +305,6 @@ def test_mid_block_finalized_slot_visibility( 7. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -405,7 +402,6 @@ def test_finalization_prunes_stale_pending_votes_and_rebases_window( 7. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -513,7 +509,6 @@ def test_stale_finalized_source_justifies_without_rewinding_finalization( 5. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -640,7 +635,6 @@ def test_source_at_finalized_boundary_justifies_without_refinalizing( 5. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -767,7 +761,6 @@ def test_non_adjacent_justification_finalizes_across_non_justifiable_gap( 7. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -848,7 +841,6 @@ def test_no_finalization_when_rebased_boundary_exposes_intermediate_justifiable_ 7. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -973,7 +965,6 @@ def test_mid_block_finalized_slot_rejects_target_that_loses_justifiability( 7. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -1079,7 +1070,6 @@ def test_merged_attestations_for_same_target_justify_and_finalize_cleanly( 7. There are no pending justifications """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -1164,7 +1154,6 @@ def test_rebased_finalization_prunes_stale_votes_and_preserves_future_votes( 8. justifications_validators contains a single 1-of-4 pending tally """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), diff --git a/tests/consensus/lstar/state_transition/test_genesis.py b/tests/consensus/lstar/state_transition/test_genesis.py index f2976a335..d4da3f4be 100644 --- a/tests/consensus/lstar/state_transition/test_genesis.py +++ b/tests/consensus/lstar/state_transition/test_genesis.py @@ -71,7 +71,6 @@ def test_genesis_default_configuration( - 4 validators with zero public_keys """ state_transition_test( - pre=generate_pre_state(), blocks=[], post=StateExpectation( slot=Slot(0), diff --git a/tests/consensus/lstar/state_transition/test_justification.py b/tests/consensus/lstar/state_transition/test_justification.py index 07ec8ee7d..46a45d56a 100644 --- a/tests/consensus/lstar/state_transition/test_justification.py +++ b/tests/consensus/lstar/state_transition/test_justification.py @@ -41,7 +41,6 @@ def test_supermajority_attestations_justify_block( 4. latest_justified_slot advances to slot 1 """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -256,7 +255,6 @@ def test_repeated_validators_do_not_double_count_across_blocks( 4. latest_justified_slot stays at genesis """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -388,7 +386,6 @@ def test_pronic_boundary_acceptance( 4. latest_justified_slot advances to slot 6 """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -442,7 +439,6 @@ def test_non_justifiable_boundary_rejection( 4. latest_justified_slot stays at genesis """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -497,7 +493,6 @@ def test_square_boundary_acceptance( 4. latest_justified_slot advances to slot 9 """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -724,7 +719,6 @@ def test_supermajority_with_mismatched_target_root_is_ignored( 3. latest_justified_slot stays at genesis """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), @@ -776,7 +770,6 @@ def test_attestation_with_target_root_not_in_historical_hashes_is_skipped( 4. latest_justified_slot stays at genesis """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -921,7 +914,6 @@ def test_target_at_or_before_source_is_ignored( 4. latest_justified_slot remains at slot 4 """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -1042,7 +1034,6 @@ def test_attestation_with_already_justified_target_is_silently_skipped( 4. The block containing the duplicate attestation is accepted as valid """ state_transition_test( - pre=generate_pre_state(), blocks=[ # Step 1 — Build chain and justify slot X BlockSpec(slot=Slot(1), label="block_1"), @@ -1128,7 +1119,6 @@ def test_attestation_with_zero_hash_source_root_is_skipped( What matters for client teams: correct post-state, no crash. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -1206,7 +1196,6 @@ def test_attestation_with_zero_hash_target_root_is_skipped( The test proves correct post-state regardless of which fires. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec( @@ -1288,7 +1277,6 @@ def test_attestation_with_unjustified_source_is_silently_skipped( The 4-entry pending votes (not 8) proves it was skipped. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), # Justify slot 1: 3/4 validators attest. @@ -1410,7 +1398,6 @@ def test_same_block_multi_target_attestations_advance_to_highest_slot( since slot 9 is later in chain history. """ state_transition_test( - pre=generate_pre_state(), blocks=[ BlockSpec(slot=Slot(1), label="block_1"), BlockSpec(slot=Slot(2), parent_label="block_1", label="block_2"), diff --git a/tests/consensus/lstar/verify_signatures/test_index_out_of_range.py b/tests/consensus/lstar/verify_signatures/test_index_out_of_range.py index 96496a58f..c83a18919 100644 --- a/tests/consensus/lstar/verify_signatures/test_index_out_of_range.py +++ b/tests/consensus/lstar/verify_signatures/test_index_out_of_range.py @@ -5,7 +5,6 @@ AggregatedAttestationSpec, BlockSpec, VerifySignaturesTestFiller, - generate_pre_state, ) from lean_spec.spec.forks import Slot, ValidatorIndex @@ -42,7 +41,6 @@ def test_attestation_validator_index_out_of_range_rejected( so the failure reason is unambiguous across clients. """ verify_signatures_test( - anchor_state=generate_pre_state(), block=BlockSpec( slot=Slot(2), attestations=[