From 410de9352c344985d65ff156f5f27c1086d28dfd Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 9 Dec 2025 12:03:00 +0700 Subject: [PATCH 01/11] feat: add VerifySignaturesTest fixture --- .../testing/src/consensus_testing/__init__.py | 4 + .../test_fixtures/__init__.py | 2 + .../test_fixtures/verify_signatures.py | 350 ++++++++++++++++++ .../test_types/block_spec.py | 13 + .../test_types/signed_attestation_spec.py | 12 + 5 files changed, 381 insertions(+) create mode 100644 packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py diff --git a/packages/testing/src/consensus_testing/__init__.py b/packages/testing/src/consensus_testing/__init__.py index 9c92bbbc..66129d94 100644 --- a/packages/testing/src/consensus_testing/__init__.py +++ b/packages/testing/src/consensus_testing/__init__.py @@ -7,6 +7,7 @@ BaseConsensusFixture, ForkChoiceTest, StateTransitionTest, + VerifySignaturesTest, ) from .test_types import ( AttestationCheck, @@ -24,6 +25,7 @@ StateTransitionTestFiller = Type[StateTransitionTest] ForkChoiceTestFiller = Type[ForkChoiceTest] +VerifySignaturesTestFiller = Type[VerifySignaturesTest] __all__ = [ # Public API @@ -36,6 +38,7 @@ "BaseConsensusFixture", "StateTransitionTest", "ForkChoiceTest", + "VerifySignaturesTest", # Test types "BaseForkChoiceStep", "TickStep", @@ -48,4 +51,5 @@ # Type aliases for test function signatures "StateTransitionTestFiller", "ForkChoiceTestFiller", + "VerifySignaturesTestFiller", ] diff --git a/packages/testing/src/consensus_testing/test_fixtures/__init__.py b/packages/testing/src/consensus_testing/test_fixtures/__init__.py index 1616c112..821b23b5 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/__init__.py +++ b/packages/testing/src/consensus_testing/test_fixtures/__init__.py @@ -3,9 +3,11 @@ from .base import BaseConsensusFixture from .fork_choice import ForkChoiceTest from .state_transition import StateTransitionTest +from .verify_signatures import VerifySignaturesTest __all__ = [ "BaseConsensusFixture", "StateTransitionTest", "ForkChoiceTest", + "VerifySignaturesTest", ] diff --git a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py new file mode 100644 index 00000000..c61b9276 --- /dev/null +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -0,0 +1,350 @@ +"""Signature test fixture format.""" + +from __future__ import annotations + +from functools import lru_cache +from typing import Any, ClassVar + +from pydantic import Field, field_serializer + +from lean_spec.subspecs.containers.attestation import ( + Attestation, + AttestationData, + SignedAttestation, +) +from lean_spec.subspecs.containers.block.block import ( + BlockWithAttestation, + SignedBlockWithAttestation, +) +from lean_spec.subspecs.containers.block.types import BlockSignatures +from lean_spec.subspecs.containers.checkpoint import Checkpoint +from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.containers.state.state import State +from lean_spec.subspecs.koalabear import Fp +from lean_spec.subspecs.ssz import hash_tree_root +from lean_spec.subspecs.xmss.interface import PROD_SIGNATURE_SCHEME +from lean_spec.types import Bytes32, Uint64 + +from ..keys import XmssKeyManager +from ..test_types import BlockSpec, SignedAttestationSpec +from .base import BaseConsensusFixture + + +@lru_cache(maxsize=1) +def _get_shared_prod_key_manager() -> XmssKeyManager: + """ + Get or create the shared XMSS key manager for reusing keys across tests. + + Uses functools.lru_cache to create a singleton instance that's shared + across all test fixture generations within a session. This optimizes + performance by reusing keys when possible. + + Returns: + Shared XmssKeyManager instance with max_slot=10. + """ + return XmssKeyManager(max_slot=Slot(10), scheme=PROD_SIGNATURE_SCHEME) + + +class VerifySignaturesTest(BaseConsensusFixture): + """ + Test fixture for verifying signatures on SignedBlockWithAttestation. + + The fixture takes a BlockSpec and optional SignedAttestationSpec inputs and generates + a complete SignedBlockWithAttestation as the test output. + + To execute test vectors produced by this fixture, simply pass the vector's + `signed_block_with_attestation` and `anchor_state` through the client's + `SignedBlockWithAttestation.verify_signatures()`. The test case is expected to fail + if `expect_exception` is set. + + Structure: + anchor_state: Initial trusted consensus state + signed_block_with_attestation: The generated SignedBlockWithAttestation + expect_exception: Expected exception for invalid tests + """ + + format_name: ClassVar[str] = "verify_signature_test" + description: ClassVar[str] = ( + "Tests signature verification for blocks with attestations through" + "SignedBlockWithAttestation.verify_signatures()" + ) + + anchor_state: State | None = None + """ + The initial consensus state before processing. + + If not provided, the framework will use the genesis fixture. + """ + + block: BlockSpec = Field(exclude=True) + """ + Block specifications to generate signatures for. + + This defines the block parameters including attestations. The framework will + build a complete signed block with all necessary signatures. + + Attestations should be specified via block.attestations as SignedAttestationSpec objects. + Use block.valid_signature to control proposer attestation signature validity. + Use block.attestations.valid_signature to control attester signature validity. + + Note: This field is excluded from the output test vector. Use signed_block_with_attestation. + """ + + signed_block_with_attestation: SignedBlockWithAttestation | None = None + """ + The generated signed block with attestation. + + This is populated by make_fixture() and contains the complete signed block + ready for verification. + """ + + expect_exception: type[Exception] | None = None + """ + Expected exception type for invalid tests. + + If provided, an exception of this type is expected during signature verification. + """ + + @field_serializer("expect_exception", when_used="json") + def serialize_exception(self, value: type[Exception] | None) -> str | None: + """Serialize exception type to string.""" + if value is None: + return None + # Format: "ExceptionClassName" (just the class name for now) + # TODO: This can be used to map exceptions to expected exceptions from clients + # as in execution-spec-tests - e.g., "StateTransitionException.INVALID_SLOT" + return value.__name__ + + def make_fixture(self) -> VerifySignaturesTest: + """ + Generate the fixture by creating a signed block with attestations. + + Builds a block from BlockSpec, generates the relevant signatures to produce + SignedBlockWithAttestation, then verifies that the signatures are valid. + + Returns: + ------- + SignatureTest + The validated fixture. + + Raises: + ------ + AssertionError + If signature verification fails. + """ + # Ensure anchor_state is set + assert self.anchor_state is not None, "anchor_state must be set before make_fixture" + + # Use shared key manager + key_manager = _get_shared_prod_key_manager() + + # Build the signed block with attestation + signed_block = self._build_block_from_spec(self.block, self.anchor_state, key_manager) + + exception_raised: Exception | None = None + + # Verify signatures + try: + signed_block.verify_signatures(self.anchor_state, scheme=PROD_SIGNATURE_SCHEME) + except AssertionError as e: + exception_raised = e + # If we expect an exception, this is fine + if self.expect_exception is None: + # Unexpected failure + raise AssertionError(f"Unexpected error verifying block signature(s): {e}") from e + finally: + # Always store filled block for serialization, even if an exception occurred + # This ensures the test fixture contains the signed block that consumer can test with + self.signed_block_with_attestation = signed_block + + # Validate exception expectations + if self.expect_exception is not None: + if exception_raised is None: + raise AssertionError( + f"Expected exception {self.expect_exception.__name__} but processing succeeded" + ) + if not isinstance(exception_raised, self.expect_exception): + raise AssertionError( + f"Expected {self.expect_exception.__name__} " + f"but got {type(exception_raised).__name__}: {exception_raised}" + ) + + return self + + def _build_block_from_spec( + self, + spec: BlockSpec, + state: State, + key_manager: XmssKeyManager, + ) -> SignedBlockWithAttestation: + """ + Build a complete SignedBlockWithAttestation from a BlockSpec. + + This method combines: + - spec logic (via the state block building logic), + - test-specific logic (signing), + to produce a complete signed block. + + Parameters + ---------- + spec : BlockSpec + The lightweight block specification. + state : State + The anchor state to build against. + key_manager : XmssKeyManager + The key manager for signing. + + Returns: + ------- + SignedBlockWithAttestation + A complete signed block with all attestations. + """ + # Determine proposer index + proposer_index = spec.proposer_index or Uint64(int(spec.slot) % int(state.validators.count)) + + # Resolve parent root + parent_state = state.process_slots(spec.slot) + parent_root = hash_tree_root(parent_state.latest_block_header) + + # Build attestations from spec + attestations, signatures = self._build_attestations_from_spec( + spec, state, key_manager + ) + + # Use State.build_block for core block building (pure spec logic) + final_block, _, _, _ = state.build_block( + slot=spec.slot, + proposer_index=proposer_index, + parent_root=parent_root, + attestations=attestations, + ) + + # Create proposer attestation for this block + block_root = hash_tree_root(final_block) + proposer_attestation = Attestation( + validator_id=proposer_index, + data=AttestationData( + slot=spec.slot, + head=Checkpoint(root=block_root, slot=spec.slot), + target=Checkpoint(root=block_root, slot=spec.slot), + source=Checkpoint(root=parent_root, slot=parent_state.latest_block_header.slot), + ), + ) + + # Sign proposer attestation - use valid or dummy signature based on spec + if spec.valid_signature: + proposer_attestation_signature = key_manager.sign_attestation(proposer_attestation) + else: + # Generate an invalid dummy signature (all zeros) + from lean_spec.subspecs.xmss.constants import PROD_CONFIG + from lean_spec.subspecs.xmss.containers import Signature + from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness + + proposer_attestation_signature = Signature( + path=HashTreeOpening(siblings=HashDigestList(data=[])), + rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), + hashes=HashDigestList(data=[]), + ) + + signatures.append(proposer_attestation_signature) + + return SignedBlockWithAttestation( + message=BlockWithAttestation( + block=final_block, + proposer_attestation=proposer_attestation, + ), + signature=BlockSignatures(data=signatures), + ) + + def _build_attestations_from_spec( + self, + spec: BlockSpec, + state: State, + key_manager: XmssKeyManager, + ) -> tuple[list[Attestation], list[Any]]: + """Build attestations list from BlockSpec.""" + if spec.attestations is None: + return [], [] + + attestations = [] + attestation_signatures = [] + + for attestation_item in spec.attestations: + if isinstance(attestation_item, SignedAttestationSpec): + signed_attestation = self._build_signed_attestation_from_spec( + attestation_item, state, key_manager + ) + attestations.append(signed_attestation.message) + attestation_signatures.append(signed_attestation.signature) + else: + attestations.append(attestation_item.message) + attestation_signatures.append(attestation_item.signature) + + return attestations, attestation_signatures + + def _build_signed_attestation_from_spec( + self, + spec: SignedAttestationSpec, + state: State, + key_manager: XmssKeyManager, + ) -> SignedAttestation: + """ + Build a SignedAttestation from a SignedAttestationSpec. + + Parameters + ---------- + spec : SignedAttestationSpec + The attestation specification to resolve. + state : State + The state to get latest_justified checkpoint from. + key_manager : XmssKeyManager + The key manager for signing. + + Returns: + ------- + SignedAttestation + The resolved signed attestation. + """ + # For this test, we use a dummy target since we're just testing signature generation + # In a real test, you would resolve target_root_label from a block registry + target_root = Bytes32.zero() + target_checkpoint = Checkpoint(root=target_root, slot=spec.target_slot) + + # Derive head = target + head_checkpoint = target_checkpoint + + # Derive source from state's latest justified checkpoint + source_checkpoint = state.latest_justified + + # Create attestation + attestation = Attestation( + validator_id=spec.validator_id, + data=AttestationData( + slot=spec.slot, + head=head_checkpoint, + target=target_checkpoint, + source=source_checkpoint, + ), + ) + + # Sign the attestation - use dummy signature if expecting invalid signature + if spec.valid_signature: + # Generate valid signature using key manager + signature = key_manager.sign_attestation(attestation) + else: + # Generate an invalid dummy signature (all zeros) + from lean_spec.subspecs.xmss.constants import PROD_CONFIG + from lean_spec.subspecs.xmss.containers import Signature + from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness + + signature = Signature( + path=HashTreeOpening(siblings=HashDigestList(data=[])), + rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), + hashes=HashDigestList(data=[]), + ) + + # Create signed attestation + return SignedAttestation( + message=attestation, + signature=signature, + ) diff --git a/packages/testing/src/consensus_testing/test_types/block_spec.py b/packages/testing/src/consensus_testing/test_types/block_spec.py index 2f564d09..cc3054ce 100644 --- a/packages/testing/src/consensus_testing/test_types/block_spec.py +++ b/packages/testing/src/consensus_testing/test_types/block_spec.py @@ -97,3 +97,16 @@ class BlockSpec(CamelModel): If None, parent is determined by the current canonical head. If specified, parent_root is computed from the labeled block. """ + + valid_signature: bool = True + """ + Flag whether the proposer's signature in generated block should be valid. + + Used for testing that verification properly rejects invalid block signatures. + When False, a structurally valid but cryptographically invalid signature + (all zeros) will be generated for the proposer attestation instead of a + proper XMSS signature. + + Defaults to True (valid signature). + If False, the proposer attestation will be given a dummy/invalid signature. + """ diff --git a/packages/testing/src/consensus_testing/test_types/signed_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/signed_attestation_spec.py index 6483e5c8..183fd58f 100644 --- a/packages/testing/src/consensus_testing/test_types/signed_attestation_spec.py +++ b/packages/testing/src/consensus_testing/test_types/signed_attestation_spec.py @@ -35,3 +35,15 @@ class SignedAttestationSpec(CamelModel): If None, uses an empty signature for testing. """ + + valid_signature: bool = True + """ + Flag whether the generated attestation signature should be valid. + + Used for testing that verification properly rejects invalid attestation signatures. + When False, a structurally valid but cryptographically invalid signature + (all zeros) will be generated for the attestation instead of a proper XMSS signature. + + Defaults to True (valid signature). + If False, the attestation will be given a dummy/invalid signature. + """ From a9f6bb8045b997b74aa9034c966fb2f1224e967e Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 9 Dec 2025 12:03:09 +0700 Subject: [PATCH 02/11] feat: add VerifySignaturesTest cases --- .../devnet/verify_signatures/__init__.py | 1 + .../test_invalid_signatures.py | 98 +++++++++++++++++++ .../test_valid_signatures.py | 95 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 tests/consensus/devnet/verify_signatures/__init__.py create mode 100644 tests/consensus/devnet/verify_signatures/test_invalid_signatures.py create mode 100644 tests/consensus/devnet/verify_signatures/test_valid_signatures.py diff --git a/tests/consensus/devnet/verify_signatures/__init__.py b/tests/consensus/devnet/verify_signatures/__init__.py new file mode 100644 index 00000000..5c9c322a --- /dev/null +++ b/tests/consensus/devnet/verify_signatures/__init__.py @@ -0,0 +1 @@ +"""Signature verification tests.""" diff --git a/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py b/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py new file mode 100644 index 00000000..f1aa4d36 --- /dev/null +++ b/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py @@ -0,0 +1,98 @@ +"""Invalid signature verification tests""" + +import pytest +from consensus_testing import ( + BlockSpec, + SignedAttestationSpec, + VerifySignaturesTestFiller, + generate_pre_state, +) + +from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.xmss.interface import PROD_SIGNATURE_SCHEME +from lean_spec.types import Uint64 + +pytestmark = pytest.mark.valid_until("Devnet") + + +def test_invalid_signature( + verify_signatures_test: VerifySignaturesTestFiller, +) -> None: + """ + Test that invalid signatures are properly rejected during verification. + + Scenario + -------- + - Single block at slot 1 + - Proposer attestation has an invalid signature + - No additional attestations (only proposer attestation) + + Expected Behavior + ----------------- + 1. Proposer's signature in SignedBlockWithAttestation is rejected + + Why This Matters + ---------------- + This test verifies the negative case: + - Signature verification actually validates cryptographic correctness + not just structural correctness. + - Invalid signatures are caught, not silently accepted + """ + verify_signatures_test( + anchor_state=generate_pre_state(num_validators=1, scheme=PROD_SIGNATURE_SCHEME), + block=BlockSpec( + slot=Slot(1), + attestations=[], + valid_signature=False, + ), + expect_exception=AssertionError, + ) + + +def test_mixed_valid_invalid_signatures( + verify_signatures_test: VerifySignaturesTestFiller, +) -> None: + """ + Test that signature verification catches invalid signatures among valid ones. + + Scenario + -------- + - Single block at slot 1 + - Proposer attestation from validator 1 + - 2 non-proposer attestations from validators 0 and 2 + - Total: 3 signatures, middle attestation (validator 2) has an invalid signature + + Expected Behavior + ----------------- + 1. The SignedBlockWithAttestation is rejected due to 1 invalid signature + + Why This Matters + ---------------- + This test verifies that signature verification: + - Checks every signature individually, not just the first or last + - Cannot be bypassed by surrounding invalid signatures with valid ones + - Properly fails even when some signatures are valid + - Validates all attestations in the block + """ + verify_signatures_test( + anchor_state=generate_pre_state(num_validators=3, scheme=PROD_SIGNATURE_SCHEME), + block=BlockSpec( + slot=Slot(1), + attestations=[ + SignedAttestationSpec( + validator_id=Uint64(0), + slot=Slot(1), + target_slot=Slot(0), + target_root_label="genesis", + ), + SignedAttestationSpec( + validator_id=Uint64(2), + slot=Slot(1), + target_slot=Slot(0), + target_root_label="genesis", + valid_signature=False, + ), + ], + ), + expect_exception=AssertionError, + ) diff --git a/tests/consensus/devnet/verify_signatures/test_valid_signatures.py b/tests/consensus/devnet/verify_signatures/test_valid_signatures.py new file mode 100644 index 00000000..6c91dac1 --- /dev/null +++ b/tests/consensus/devnet/verify_signatures/test_valid_signatures.py @@ -0,0 +1,95 @@ +"""Valid signature verification tests""" + +import pytest +from consensus_testing import ( + BlockSpec, + SignedAttestationSpec, + VerifySignaturesTestFiller, + generate_pre_state, +) + +from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.xmss.interface import PROD_SIGNATURE_SCHEME +from lean_spec.types import Uint64 + +pytestmark = pytest.mark.valid_until("Devnet") + + +def test_proposer_signature( + verify_signatures_test: VerifySignaturesTestFiller, +) -> None: + """ + Test valid proposer signature in SignedBlockWithAttestation. + + Scenario + -------- + - Single block at slot 1 + - No additional attestations (only proposer attestation) + + Expected Behavior + ----------------- + 1. Proposer's signature in SignedBlockWithAttestation can be verified against + the validator's pubkey in the state + + Why This Matters + ---------------- + This is the most basic signature generation test. It verifies: + - XMSS key generation works + - Signature aggregation includes proposer signature + """ + verify_signatures_test( + anchor_state=generate_pre_state(num_validators=2, scheme=PROD_SIGNATURE_SCHEME), + block=BlockSpec( + slot=Slot(1), + attestations=[], + ), + ) + + +def test_proposer_and_attester_signatures( + verify_signatures_test: VerifySignaturesTestFiller, +) -> None: + """ + Test valid proposer and attester signatures in SignedBlockWithAttestation. + + Scenario + -------- + - Single block at slot 1 + - 3 validators in the genesis state + - 2 additional attestations from validators 0 and 2 (in addition to proposer) + - Verifies that all signatures are generated correctly + + Expected Behavior + ----------------- + 1. Proposer's signature in SignedBlockWithAttestation can be verified against + the validator's pubkey in the state + 2. Attester's signatures in SignedBlockWithAttestation can be verified against + the validator's pubkey in the state + + Why This Matters + ---------------- + This test verifies multi-validator signature scenarios: + - Multiple XMSS keys are generated for different validators + - Attestations from non-proposer validators are correctly verified + - Signature aggregation works with multiple attestations (signature positions are correct) + """ + verify_signatures_test( + anchor_state=generate_pre_state(num_validators=3, scheme=PROD_SIGNATURE_SCHEME), + block=BlockSpec( + slot=Slot(1), + attestations=[ + SignedAttestationSpec( + validator_id=Uint64(0), + slot=Slot(1), + target_slot=Slot(0), + target_root_label="genesis", + ), + SignedAttestationSpec( + validator_id=Uint64(2), + slot=Slot(1), + target_slot=Slot(0), + target_root_label="genesis", + ), + ], + ), + ) From 448ef332ae53d8246db5d777838e245567570238 Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 9 Dec 2025 12:04:19 +0700 Subject: [PATCH 03/11] fix: VerifySignatureTest format name --- .../src/consensus_testing/test_fixtures/verify_signatures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c61b9276..74d1e410 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -63,7 +63,7 @@ class VerifySignaturesTest(BaseConsensusFixture): expect_exception: Expected exception for invalid tests """ - format_name: ClassVar[str] = "verify_signature_test" + format_name: ClassVar[str] = "verify_signatures_test" description: ClassVar[str] = ( "Tests signature verification for blocks with attestations through" "SignedBlockWithAttestation.verify_signatures()" From 11d88a0d86109b2db33efba068ab4408120243a9 Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 9 Dec 2025 12:05:26 +0700 Subject: [PATCH 04/11] fix: remove remaining prod references --- .../test_fixtures/verify_signatures.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 74d1e410..dd434282 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -42,7 +42,7 @@ def _get_shared_prod_key_manager() -> XmssKeyManager: Returns: Shared XmssKeyManager instance with max_slot=10. """ - return XmssKeyManager(max_slot=Slot(10), scheme=PROD_SIGNATURE_SCHEME) + return XmssKeyManager(max_slot=Slot(10)) class VerifySignaturesTest(BaseConsensusFixture): @@ -145,7 +145,7 @@ def make_fixture(self) -> VerifySignaturesTest: # Verify signatures try: - signed_block.verify_signatures(self.anchor_state, scheme=PROD_SIGNATURE_SCHEME) + signed_block.verify_signatures(self.anchor_state) except AssertionError as e: exception_raised = e # If we expect an exception, this is fine @@ -236,13 +236,13 @@ def _build_block_from_spec( proposer_attestation_signature = key_manager.sign_attestation(proposer_attestation) else: # Generate an invalid dummy signature (all zeros) - from lean_spec.subspecs.xmss.constants import PROD_CONFIG + from lean_spec.subspecs.xmss.constants import TEST_CONFIG from lean_spec.subspecs.xmss.containers import Signature from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness proposer_attestation_signature = Signature( path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), + rho=Randomness(data=[Fp(0) for _ in range(TEST_CONFIG.RAND_LEN_FE)]), hashes=HashDigestList(data=[]), ) @@ -333,13 +333,13 @@ def _build_signed_attestation_from_spec( signature = key_manager.sign_attestation(attestation) else: # Generate an invalid dummy signature (all zeros) - from lean_spec.subspecs.xmss.constants import PROD_CONFIG + from lean_spec.subspecs.xmss.constants import TEST_CONFIG from lean_spec.subspecs.xmss.containers import Signature from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness signature = Signature( path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), + rho=Randomness(data=[Fp(0) for _ in range(TEST_CONFIG.RAND_LEN_FE)]), hashes=HashDigestList(data=[]), ) From 173c0102278a835cf91c5cc432f0ce72dc576b7a Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 9 Dec 2025 12:07:38 +0700 Subject: [PATCH 05/11] fix: remaining reference to prod --- .../src/consensus_testing/test_fixtures/verify_signatures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 dd434282..0a082145 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -31,7 +31,7 @@ @lru_cache(maxsize=1) -def _get_shared_prod_key_manager() -> XmssKeyManager: +def _get_shared_key_manager() -> XmssKeyManager: """ Get or create the shared XMSS key manager for reusing keys across tests. @@ -136,7 +136,7 @@ def make_fixture(self) -> VerifySignaturesTest: assert self.anchor_state is not None, "anchor_state must be set before make_fixture" # Use shared key manager - key_manager = _get_shared_prod_key_manager() + key_manager = _get_shared_key_manager() # Build the signed block with attestation signed_block = self._build_block_from_spec(self.block, self.anchor_state, key_manager) From 0fcc495dfbb163d45e37b6cda18474944ca7a565 Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 9 Dec 2025 12:37:07 +0700 Subject: [PATCH 06/11] fix: linting --- .../src/consensus_testing/test_fixtures/verify_signatures.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 0a082145..953651c2 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -22,7 +22,6 @@ from lean_spec.subspecs.containers.state.state import State from lean_spec.subspecs.koalabear import Fp from lean_spec.subspecs.ssz import hash_tree_root -from lean_spec.subspecs.xmss.interface import PROD_SIGNATURE_SCHEME from lean_spec.types import Bytes32, Uint64 from ..keys import XmssKeyManager @@ -207,9 +206,7 @@ def _build_block_from_spec( parent_root = hash_tree_root(parent_state.latest_block_header) # Build attestations from spec - attestations, signatures = self._build_attestations_from_spec( - spec, state, key_manager - ) + attestations, signatures = self._build_attestations_from_spec(spec, state, key_manager) # Use State.build_block for core block building (pure spec logic) final_block, _, _, _ = state.build_block( From 6d53c2b056dbfa006a672b566cb705331f5f0ff3 Mon Sep 17 00:00:00 2001 From: unnawut Date: Tue, 9 Dec 2025 16:48:18 +0700 Subject: [PATCH 07/11] fix: remove remaining prod sig references --- .../devnet/verify_signatures/test_invalid_signatures.py | 5 ++--- .../devnet/verify_signatures/test_valid_signatures.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py b/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py index f1aa4d36..98ee16e2 100644 --- a/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py +++ b/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py @@ -9,7 +9,6 @@ ) from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.xmss.interface import PROD_SIGNATURE_SCHEME from lean_spec.types import Uint64 pytestmark = pytest.mark.valid_until("Devnet") @@ -39,7 +38,7 @@ def test_invalid_signature( - Invalid signatures are caught, not silently accepted """ verify_signatures_test( - anchor_state=generate_pre_state(num_validators=1, scheme=PROD_SIGNATURE_SCHEME), + anchor_state=generate_pre_state(num_validators=1), block=BlockSpec( slot=Slot(1), attestations=[], @@ -75,7 +74,7 @@ def test_mixed_valid_invalid_signatures( - Validates all attestations in the block """ verify_signatures_test( - anchor_state=generate_pre_state(num_validators=3, scheme=PROD_SIGNATURE_SCHEME), + anchor_state=generate_pre_state(num_validators=3), block=BlockSpec( slot=Slot(1), attestations=[ diff --git a/tests/consensus/devnet/verify_signatures/test_valid_signatures.py b/tests/consensus/devnet/verify_signatures/test_valid_signatures.py index 6c91dac1..e9b01298 100644 --- a/tests/consensus/devnet/verify_signatures/test_valid_signatures.py +++ b/tests/consensus/devnet/verify_signatures/test_valid_signatures.py @@ -9,7 +9,6 @@ ) from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.xmss.interface import PROD_SIGNATURE_SCHEME from lean_spec.types import Uint64 pytestmark = pytest.mark.valid_until("Devnet") @@ -38,7 +37,7 @@ def test_proposer_signature( - Signature aggregation includes proposer signature """ verify_signatures_test( - anchor_state=generate_pre_state(num_validators=2, scheme=PROD_SIGNATURE_SCHEME), + anchor_state=generate_pre_state(num_validators=2), block=BlockSpec( slot=Slot(1), attestations=[], @@ -74,7 +73,7 @@ def test_proposer_and_attester_signatures( - Signature aggregation works with multiple attestations (signature positions are correct) """ verify_signatures_test( - anchor_state=generate_pre_state(num_validators=3, scheme=PROD_SIGNATURE_SCHEME), + anchor_state=generate_pre_state(num_validators=3), block=BlockSpec( slot=Slot(1), attestations=[ From 0097cfbb15c6a1eccdc9d34321623dfa086d4c99 Mon Sep 17 00:00:00 2001 From: Unnawut Leepaisalsuwanna <921194+unnawut@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:05:05 +0700 Subject: [PATCH 08/11] Update packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py Co-authored-by: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> --- .../src/consensus_testing/test_fixtures/verify_signatures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 953651c2..93d064e5 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -132,7 +132,7 @@ def make_fixture(self) -> VerifySignaturesTest: If signature verification fails. """ # Ensure anchor_state is set - assert self.anchor_state is not None, "anchor_state must be set before make_fixture" + assert self.anchor_state is not None, "anchor state must be set before making the fixture" # Use shared key manager key_manager = _get_shared_key_manager() From ae54e112d6bb12bcd72adf54093c952c94c5a9ad Mon Sep 17 00:00:00 2001 From: Unnawut Leepaisalsuwanna <921194+unnawut@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:05:18 +0700 Subject: [PATCH 09/11] Update packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py Co-authored-by: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> --- .../src/consensus_testing/test_fixtures/verify_signatures.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 93d064e5..b0c6dde4 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -64,8 +64,7 @@ class VerifySignaturesTest(BaseConsensusFixture): format_name: ClassVar[str] = "verify_signatures_test" description: ClassVar[str] = ( - "Tests signature verification for blocks with attestations through" - "SignedBlockWithAttestation.verify_signatures()" + "Tests signature verification for blocks with attestations." ) anchor_state: State | None = None From 6a90eaac27b65d98daadd5ac1c4ef17b42886531 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 10 Dec 2025 15:14:43 +0700 Subject: [PATCH 10/11] fix: remove references from doc comments --- .../test_fixtures/verify_signatures.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) 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 b0c6dde4..bcba401d 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -51,10 +51,8 @@ class VerifySignaturesTest(BaseConsensusFixture): The fixture takes a BlockSpec and optional SignedAttestationSpec inputs and generates a complete SignedBlockWithAttestation as the test output. - To execute test vectors produced by this fixture, simply pass the vector's - `signed_block_with_attestation` and `anchor_state` through the client's - `SignedBlockWithAttestation.verify_signatures()`. The test case is expected to fail - if `expect_exception` is set. + Use the generated test vectors to test that client implementation can verify signatures + of the generated signed block with attestation.. Structure: anchor_state: Initial trusted consensus state @@ -79,21 +77,12 @@ class VerifySignaturesTest(BaseConsensusFixture): Block specifications to generate signatures for. This defines the block parameters including attestations. The framework will - build a complete signed block with all necessary signatures. - - Attestations should be specified via block.attestations as SignedAttestationSpec objects. - Use block.valid_signature to control proposer attestation signature validity. - Use block.attestations.valid_signature to control attester signature validity. - - Note: This field is excluded from the output test vector. Use signed_block_with_attestation. + build a complete signed block with attestation with all necessary signatures. """ signed_block_with_attestation: SignedBlockWithAttestation | None = None """ The generated signed block with attestation. - - This is populated by make_fixture() and contains the complete signed block - ready for verification. """ expect_exception: type[Exception] | None = None From 4ece162b7231d73b32b1a4068e1f1acb29d6a923 Mon Sep 17 00:00:00 2001 From: unnawut Date: Wed, 10 Dec 2025 15:16:10 +0700 Subject: [PATCH 11/11] fix: linting --- .../src/consensus_testing/test_fixtures/verify_signatures.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 bcba401d..f9c53c3f 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -61,9 +61,7 @@ class VerifySignaturesTest(BaseConsensusFixture): """ format_name: ClassVar[str] = "verify_signatures_test" - description: ClassVar[str] = ( - "Tests signature verification for blocks with attestations." - ) + description: ClassVar[str] = "Tests signature verification for blocks with attestations." anchor_state: State | None = None """