diff --git a/packages/testing/src/consensus_testing/__init__.py b/packages/testing/src/consensus_testing/__init__.py index 97b195ef..8f476035 100644 --- a/packages/testing/src/consensus_testing/__init__.py +++ b/packages/testing/src/consensus_testing/__init__.py @@ -16,6 +16,7 @@ SlotClockTest, SSZTest, StateTransitionTest, + SyncTest, VerifySignaturesTest, ) from .test_types import ( @@ -46,6 +47,7 @@ DiscoveryCryptoTestFiller = Type[DiscoveryCryptoTest] JustifiabilityTestFiller = Type[JustifiabilityTest] PoseidonPermutationTestFiller = Type[PoseidonPermutationTest] +SyncTestFiller = Type[SyncTest] __all__ = [ # Public API @@ -70,6 +72,7 @@ "DiscoveryCryptoTest", "JustifiabilityTest", "PoseidonPermutationTest", + "SyncTest", # Test types "BaseForkChoiceStep", "TickStep", @@ -93,4 +96,5 @@ "DiscoveryCryptoTestFiller", "JustifiabilityTestFiller", "PoseidonPermutationTestFiller", + "SyncTestFiller", ] diff --git a/packages/testing/src/consensus_testing/test_fixtures/__init__.py b/packages/testing/src/consensus_testing/test_fixtures/__init__.py index efdb9340..9bd03d63 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/__init__.py +++ b/packages/testing/src/consensus_testing/test_fixtures/__init__.py @@ -11,6 +11,7 @@ from .slot_clock import SlotClockTest from .ssz import SSZTest from .state_transition import StateTransitionTest +from .sync import SyncTest from .verify_signatures import VerifySignaturesTest __all__ = [ @@ -26,4 +27,5 @@ "DiscoveryCryptoTest", "JustifiabilityTest", "PoseidonPermutationTest", + "SyncTest", ] diff --git a/packages/testing/src/consensus_testing/test_fixtures/sync.py b/packages/testing/src/consensus_testing/test_fixtures/sync.py new file mode 100644 index 00000000..87493322 --- /dev/null +++ b/packages/testing/src/consensus_testing/test_fixtures/sync.py @@ -0,0 +1,76 @@ +"""Sync layer test fixture format. + +Emits JSON vectors for the client-facing sync helpers. Each vector +pins the expected verdict on a given input so clients can align their +sync-layer decisions bit-for-bit. +""" + +from typing import Any, ClassVar + +from lean_spec.subspecs.sync.checkpoint_sync import verify_checkpoint_state +from lean_spec.types import Uint64 + +from ..genesis import generate_pre_state +from .base import BaseConsensusFixture + + +class SyncTest(BaseConsensusFixture): + """Fixture for sync-layer conformance. + + Currently supports one operation: + + - ``verify_checkpoint``: emits the SSZ-encoded anchor state plus the + verification verdict a client must produce. + + JSON output: operation, input, output. + """ + + format_name: ClassVar[str] = "sync" + description: ClassVar[str] = "Tests sync-layer helpers clients must reproduce" + + operation: str + """Sync operation: currently only verify_checkpoint.""" + + input: dict[str, Any] + """Operation-specific input. See per-handler docstrings.""" + + output: dict[str, Any] = {} + """Computed output. Filled by make_fixture.""" + + def make_fixture(self) -> "SyncTest": + """Dispatch to the operation handler. + + Returns: + A copy of this fixture with output populated. + + Raises: + ValueError: If the operation name is unknown. + """ + if self.operation == "verify_checkpoint": + output = self._make_verify_checkpoint() + else: + raise ValueError(f"Unknown sync operation: {self.operation!r}") + return self.model_copy(update={"output": output}) + + def _make_verify_checkpoint(self) -> dict[str, Any]: + """Build a genesis state for the given validator count and report the verdict. + + Input keys: + + - ``numValidators``: number of validators in the genesis state. + + Output: + + - ``valid``: result of verify_checkpoint_state on the built state. + - ``stateBytes``: SSZ-encoded state hex, so clients can deserialize + and run their own verify_checkpoint_state. + - ``validatorCount``: echoed for diagnostic clarity. + """ + num_validators = int(self.input["numValidators"]) + state = generate_pre_state(genesis_time=Uint64(0), num_validators=num_validators) + valid = verify_checkpoint_state(state) + return { + "valid": valid, + "stateBytes": "0x" + state.encode_bytes().hex(), + "validatorCount": num_validators, + } diff --git a/tests/consensus/devnet/sync/__init__.py b/tests/consensus/devnet/sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/consensus/devnet/sync/test_checkpoint_verify.py b/tests/consensus/devnet/sync/test_checkpoint_verify.py new file mode 100644 index 00000000..39084950 --- /dev/null +++ b/tests/consensus/devnet/sync/test_checkpoint_verify.py @@ -0,0 +1,57 @@ +"""Checkpoint-sync state verification: known-answer vectors. + +Pins the structural-validity verdict each client must produce when +fetching an anchor state from a checkpoint provider. The verdict is a +defence-in-depth check applied before the state seeds a fork-choice +store. +""" + +import pytest +from consensus_testing import SyncTestFiller + +pytestmark = pytest.mark.valid_until("Devnet") + + +def test_checkpoint_verify_rejects_empty_validator_set( + sync: SyncTestFiller, +) -> None: + """A checkpoint state with zero validators is rejected. + + A state without validators cannot produce blocks, so seeding a + fork-choice store with it would be useless and mask configuration + errors. Clients must refuse the anchor before any store setup. + """ + sync( + operation="verify_checkpoint", + input={"numValidators": 0}, + ) + + +def test_checkpoint_verify_accepts_small_validator_set( + sync: SyncTestFiller, +) -> None: + """A checkpoint state with a small in-range validator set is accepted. + + Four validators is the baseline size used throughout the consensus + test suite. Pins the happy path of the verifier so clients observe + the accepted branch in addition to the rejection branch above. + """ + sync( + operation="verify_checkpoint", + input={"numValidators": 4}, + ) + + +def test_checkpoint_verify_accepts_eight_validator_set( + sync: SyncTestFiller, +) -> None: + """Eight-validator anchor state is accepted at the key-manager limit. + + Matches the maximum-validator setup used by the existing fork-choice + and signature-verification suites. Pins the verdict at the upper + end of the practical test envelope. + """ + sync( + operation="verify_checkpoint", + input={"numValidators": 8}, + )