diff --git a/packages/testing/src/consensus_testing/__init__.py b/packages/testing/src/consensus_testing/__init__.py index 66129d94..1dbe4000 100644 --- a/packages/testing/src/consensus_testing/__init__.py +++ b/packages/testing/src/consensus_testing/__init__.py @@ -10,6 +10,7 @@ VerifySignaturesTest, ) from .test_types import ( + AggregatedAttestationSpec, AttestationCheck, AttestationStep, BaseForkChoiceStep, @@ -29,6 +30,7 @@ __all__ = [ # Public API + "AggregatedAttestationSpec", "BlockSpec", "SignedAttestationSpec", "forks", 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 fdf9eff3..e03423da 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py +++ b/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py @@ -10,7 +10,6 @@ from lean_spec.subspecs.containers.attestation import ( Attestation, AttestationData, - SignedAttestation, ) from lean_spec.subspecs.containers.block import ( Block, @@ -36,11 +35,11 @@ from ..keys import LEAN_ENV_TO_SCHEMES, XmssKeyManager, get_shared_key_manager from ..test_types import ( + AggregatedAttestationSpec, AttestationStep, BlockSpec, BlockStep, ForkChoiceStep, - SignedAttestationSpec, TickStep, ) from .base import BaseConsensusFixture @@ -405,7 +404,7 @@ def _build_attestations_from_spec( parent_root: Bytes32, key_manager: XmssKeyManager, ) -> tuple[list[Attestation], dict[SignatureKey, Signature]]: - """Build attestations list from BlockSpec and their signatures.""" + """Build aggregated attestations from BlockSpec and their signatures.""" if spec.attestations is None: return [], {} @@ -413,35 +412,51 @@ def _build_attestations_from_spec( attestations = [] signature_lookup: dict[SignatureKey, Signature] = {} - for att_spec in spec.attestations: - if isinstance(att_spec, SignedAttestationSpec): - signed_att = self._build_signed_attestation_from_spec( - att_spec, block_registry, parent_state, key_manager + for aggregated_spec in spec.attestations: + # Build attestation data (shared across all validators in this aggregation spec) + attestation_data = self._build_attestation_data_from_spec( + aggregated_spec, block_registry, parent_state + ) + + # Create individual attestations and signatures for each validator + for validator_id in aggregated_spec.validator_ids: + attestation = Attestation( + validator_id=validator_id, + data=attestation_data, ) - else: - signed_att = att_spec + attestations.append(attestation) + + # Sign the attestation + if aggregated_spec.valid_signature: + signature = key_manager.sign_attestation_data( + validator_id, + attestation_data, + ) + else: + signature = Signature( + path=HashTreeOpening(siblings=HashDigestList(data=[])), + rho=Randomness(data=[Fp(0) for _ in range(Randomness.LENGTH)]), + hashes=HashDigestList(data=[]), + ) - attestation = Attestation(validator_id=signed_att.validator_id, data=signed_att.message) - attestations.append(attestation) - sig_key = SignatureKey(attestation.validator_id, attestation.data.data_root_bytes()) - signature_lookup[sig_key] = signed_att.signature + sig_key = SignatureKey(validator_id, attestation_data.data_root_bytes()) + signature_lookup[sig_key] = signature return attestations, signature_lookup - def _build_signed_attestation_from_spec( + def _build_attestation_data_from_spec( self, - spec: SignedAttestationSpec, + spec: AggregatedAttestationSpec, block_registry: dict[str, Block], state: State, - key_manager: XmssKeyManager, - ) -> SignedAttestation: + ) -> AttestationData: """ - Build a SignedAttestation from a SignedAttestationSpec. + Build AttestationData from an AggregatedAttestationSpec. Parameters ---------- - spec : SignedAttestationSpec - The attestation specification to resolve. + spec : AggregatedAttestationSpec + The aggregated attestation specification. block_registry : dict[str, Block] Registry of labeled blocks for resolving target_root_label. state : State @@ -449,8 +464,8 @@ def _build_signed_attestation_from_spec( Returns: ------- - SignedAttestation - The resolved signed attestation. + AttestationData + The attestation data shared by all validators in this aggregation. """ # Resolve target checkpoint from label if spec.target_root_label not in block_registry: @@ -468,33 +483,9 @@ def _build_signed_attestation_from_spec( # 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, - ), - ) - - # Create signed attestation - if spec.signature is not None: - signature = spec.signature - elif spec.valid_signature: - signature = key_manager.sign_attestation_data( - attestation.validator_id, attestation.data - ) - else: - signature = Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(Randomness.LENGTH)]), - hashes=HashDigestList(data=[]), - ) - - return SignedAttestation( - validator_id=attestation.validator_id, - message=attestation.data, - signature=signature, + return AttestationData( + slot=spec.slot, + head=head_checkpoint, + target=target_checkpoint, + source=source_checkpoint, ) 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 6a5c66b2..eca66f27 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -7,31 +7,77 @@ from pydantic import Field, field_serializer from lean_spec.subspecs.containers.attestation import ( + AggregatedAttestation, + AggregationBits, Attestation, AttestationData, - SignedAttestation, ) from lean_spec.subspecs.containers.block import ( BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation, ) -from lean_spec.subspecs.containers.block.types import AttestationSignatures +from lean_spec.subspecs.containers.block.types import ( + AggregatedAttestations, + AttestationSignatures, +) from lean_spec.subspecs.containers.checkpoint import Checkpoint 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.aggregation import SignatureKey +from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof, SignatureKey from lean_spec.subspecs.xmss.constants import TARGET_CONFIG from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness +from lean_spec.subspecs.xmss.types import ( + HashDigestList, + HashDigestVector, + HashTreeOpening, + Randomness, +) from lean_spec.types import Bytes32, Uint64 +from lean_spec.types.byte_arrays import ByteListMiB from ..keys import XmssKeyManager, get_shared_key_manager -from ..test_types import BlockSpec, SignedAttestationSpec +from ..test_types import AggregatedAttestationSpec, BlockSpec from .base import BaseConsensusFixture +def _create_dummy_signature() -> Signature: + """ + Create a structurally valid but cryptographically invalid individual signature. + + The signature has proper structure (correct number of siblings, hashes, etc.) + but all values are zeros, so it will fail cryptographic verification. + """ + # Create zero-filled hash digests with correct dimensions + zero_digest = HashDigestVector(data=[Fp(0) for _ in range(TARGET_CONFIG.HASH_LEN_FE)]) + + # Path needs LOG_LIFETIME siblings for the Merkle authentication path + siblings = HashDigestList(data=[zero_digest for _ in range(TARGET_CONFIG.LOG_LIFETIME)]) + + # Hashes need DIMENSION vectors for the Winternitz chain hashes + hashes = HashDigestList(data=[zero_digest for _ in range(TARGET_CONFIG.DIMENSION)]) + + return Signature( + path=HashTreeOpening(siblings=siblings), + rho=Randomness(data=[Fp(0) for _ in range(TARGET_CONFIG.RAND_LEN_FE)]), + hashes=hashes, + ) + + +def _create_dummy_aggregated_proof(validator_ids: list[Uint64]) -> AggregatedSignatureProof: + """ + Create a dummy aggregated signature proof with invalid proof data. + + The proof has the correct participants bitfield but invalid proof bytes, + so it will fail verification. + """ + return AggregatedSignatureProof( + participants=AggregationBits.from_validator_indices(validator_ids), + proof_data=ByteListMiB(data=b"\x00" * 32), # Invalid proof bytes + ) + + class VerifySignaturesTest(BaseConsensusFixture): """ Test fixture for verifying signatures on SignedBlockWithAttestation. @@ -179,29 +225,53 @@ def _build_block_from_spec( parent_state = state.process_slots(spec.slot) parent_root = hash_tree_root(parent_state.latest_block_header) - # Build attestations from spec - attestations, attestation_signature_inputs = self._build_attestations_from_spec( + # Build attestations from spec - only valid ones go through aggregation + valid_attestations, valid_signatures, invalid_specs = self._build_attestations_from_spec( spec, state, key_manager ) - # Provide signatures to State.build_block so it can include attestations during - # fixed-point collection when available_attestations/known_block_roots are used. - # This might contain invalid signatures as we are not validating them here. + # Provide signatures to State.build_block for valid attestations gossip_signatures = { SignatureKey(att.validator_id, att.data.data_root_bytes()): sig - for att, sig in zip(attestations, attestation_signature_inputs, strict=True) + for att, sig in zip(valid_attestations, valid_signatures, strict=True) } - # Use State.build_block for core block building (pure spec logic) + # Use State.build_block for valid attestations (pure spec logic) final_block, _, _, aggregated_signatures = state.build_block( slot=spec.slot, proposer_index=proposer_index, parent_root=parent_root, - attestations=attestations, + attestations=valid_attestations, gossip_signatures=gossip_signatures, aggregated_payloads={}, ) + # Create dummy proofs for invalid attestation specs + for invalid_spec in invalid_specs: + attestation_data = self._build_attestation_data_from_spec(invalid_spec, state) + + # Create aggregated attestation with dummy proof + aggregation_bits = AggregationBits.from_validator_indices(invalid_spec.validator_ids) + invalid_aggregated = AggregatedAttestation( + aggregation_bits=aggregation_bits, + data=attestation_data, + ) + invalid_proof = _create_dummy_aggregated_proof(invalid_spec.validator_ids) + + # Add to block's attestations + final_block = final_block.model_copy( + update={ + "body": final_block.body.model_copy( + update={ + "attestations": AggregatedAttestations( + data=[*final_block.body.attestations.data, invalid_aggregated] + ) + } + ) + } + ) + aggregated_signatures.append(invalid_proof) + attestation_signatures = AttestationSignatures( data=aggregated_signatures, ) @@ -225,12 +295,7 @@ def _build_block_from_spec( proposer_attestation.data, ) else: - # Generate a structurally valid but cryptographically invalid signature (all zeros). - proposer_attestation_signature = Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(TARGET_CONFIG.RAND_LEN_FE)]), - hashes=HashDigestList(data=[]), - ) + proposer_attestation_signature = _create_dummy_signature() return SignedBlockWithAttestation( message=BlockWithAttestation( @@ -248,61 +313,68 @@ def _build_attestations_from_spec( spec: BlockSpec, state: State, key_manager: XmssKeyManager, - ) -> tuple[list[Attestation], list[Any]]: - """Build attestations list from BlockSpec.""" + ) -> tuple[list[Attestation], list[Any], list[AggregatedAttestationSpec]]: + """ + Build attestations list from BlockSpec. + + Returns: + ------- + tuple of: + - valid_attestations: Attestations with valid signatures for aggregation + - valid_signatures: Corresponding signatures for valid attestations + - invalid_specs: Specs with valid_signature=False (handled separately) + """ if spec.attestations is None: - return [], [] + return [], [], [] - attestations = [] - attestation_signatures = [] + valid_attestations = [] + valid_signatures = [] + invalid_specs = [] - for attestation_item in spec.attestations: - if isinstance(attestation_item, SignedAttestationSpec): - signed_attestation = self._build_signed_attestation_from_spec( - attestation_item, state, key_manager - ) - # Reconstruct Attestation from SignedAttestation components - attestations.append( + for aggregated_spec in spec.attestations: + if not aggregated_spec.valid_signature: + # Defer invalid specs - they'll get dummy proofs created directly + invalid_specs.append(aggregated_spec) + continue + + # Build attestation data (shared across all validators in this group) + attestation_data = self._build_attestation_data_from_spec(aggregated_spec, state) + + # Create individual attestations and signatures for each validator + for validator_id in aggregated_spec.validator_ids: + valid_attestations.append( Attestation( - validator_id=signed_attestation.validator_id, - data=signed_attestation.message, + validator_id=validator_id, + data=attestation_data, ) ) - attestation_signatures.append(signed_attestation.signature) - else: - # Reconstruct Attestation from existing SignedAttestation - attestations.append( - Attestation( - validator_id=attestation_item.validator_id, - data=attestation_item.message, - ) + signature = key_manager.sign_attestation_data( + validator_id, + attestation_data, ) - attestation_signatures.append(attestation_item.signature) + valid_signatures.append(signature) - return attestations, attestation_signatures + return valid_attestations, valid_signatures, invalid_specs - def _build_signed_attestation_from_spec( + def _build_attestation_data_from_spec( self, - spec: SignedAttestationSpec, + spec: AggregatedAttestationSpec, state: State, - key_manager: XmssKeyManager, - ) -> SignedAttestation: + ) -> AttestationData: """ - Build a SignedAttestation from a SignedAttestationSpec. + Build AttestationData from an AggregatedAttestationSpec. Parameters ---------- - spec : SignedAttestationSpec - The attestation specification to resolve. + spec : AggregatedAttestationSpec + The aggregated attestation specification. state : State The state to get latest_justified checkpoint from. - key_manager : XmssKeyManager - The key manager for signing. Returns: ------- - SignedAttestation - The resolved signed attestation. + AttestationData + The attestation data shared by all validators in this aggregation. """ # 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 @@ -315,35 +387,9 @@ def _build_signed_attestation_from_spec( # 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_data( - attestation.validator_id, - attestation.data, - ) - else: - # Generate a structurally valid but cryptographically invalid signature (all zeros). - signature = Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(TARGET_CONFIG.RAND_LEN_FE)]), - hashes=HashDigestList(data=[]), - ) - - # Create signed attestation - return SignedAttestation( - validator_id=attestation.validator_id, - message=attestation.data, - signature=signature, + return AttestationData( + slot=spec.slot, + head=head_checkpoint, + target=target_checkpoint, + source=source_checkpoint, ) diff --git a/packages/testing/src/consensus_testing/test_types/__init__.py b/packages/testing/src/consensus_testing/test_types/__init__.py index 4ebf7208..c0bd0b93 100644 --- a/packages/testing/src/consensus_testing/test_types/__init__.py +++ b/packages/testing/src/consensus_testing/test_types/__init__.py @@ -1,5 +1,6 @@ """Test types for consensus test fixtures.""" +from .aggregated_attestation_spec import AggregatedAttestationSpec from .block_spec import BlockSpec from .genesis import generate_pre_state from .signed_attestation_spec import SignedAttestationSpec @@ -14,6 +15,7 @@ from .store_checks import AttestationCheck, StoreChecks __all__ = [ + "AggregatedAttestationSpec", "StateExpectation", "StoreChecks", "AttestationCheck", diff --git a/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py new file mode 100644 index 00000000..fa8b2905 --- /dev/null +++ b/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py @@ -0,0 +1,40 @@ +"""Lightweight aggregated attestation specification for test definitions.""" + +from lean_spec.subspecs.containers.slot import Slot +from lean_spec.types import CamelModel, Uint64 + + +class AggregatedAttestationSpec(CamelModel): + """ + Aggregated attestation specification for test definitions. + + Specifies multiple validators attesting to the same data. + Head and source are automatically derived from target. + """ + + validator_ids: list[Uint64] + """The indices of validators making the attestation (required).""" + + slot: Slot + """The slot for which the attestation is made (required).""" + + target_slot: Slot + """The slot of the target block being attested to (required).""" + + target_root_label: str + """ + Label referencing a previously created block as the target (required). + + The block must exist in the block registry with this label. + """ + + valid_signature: bool = True + """ + Flag whether the generated attestation signatures should be valid. + + Used for testing that verification properly rejects invalid attestation signatures. + When False, structurally valid but cryptographically invalid signatures + (all zeros) will be generated for all attestations instead of proper XMSS signatures. + + Defaults to True (valid signatures). + """ 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 3efbab79..95af44ad 100644 --- a/packages/testing/src/consensus_testing/test_types/block_spec.py +++ b/packages/testing/src/consensus_testing/test_types/block_spec.py @@ -1,13 +1,10 @@ """Lightweight block specification for test definitions.""" -from typing import Union - -from lean_spec.subspecs.containers.attestation import SignedAttestation from lean_spec.subspecs.containers.block import BlockBody from lean_spec.subspecs.containers.slot import Slot from lean_spec.types import Bytes32, CamelModel, Uint64 -from .signed_attestation_spec import SignedAttestationSpec +from .aggregated_attestation_spec import AggregatedAttestationSpec class BlockSpec(CamelModel): @@ -59,12 +56,12 @@ class BlockSpec(CamelModel): Note: If body is provided, attestations field is ignored. """ - attestations: list[Union[SignedAttestation, SignedAttestationSpec]] | None = None + attestations: list[AggregatedAttestationSpec] | None = None """ - List of signed attestations to include in this block's body. + List of aggregated attestations to include in this block's body. - These attestations will be included in block.body.attestations. - Can be either SignedAttestation (direct) or SignedAttestationSpec. + Each entry specifies multiple validators attesting to the same data. + The framework generates signatures and aggregates them. If None, framework uses default behavior (empty body). If body is provided, this field is ignored. diff --git a/tests/consensus/devnet/fc/test_fork_choice_reorgs.py b/tests/consensus/devnet/fc/test_fork_choice_reorgs.py index 67726257..8c98f8bf 100644 --- a/tests/consensus/devnet/fc/test_fork_choice_reorgs.py +++ b/tests/consensus/devnet/fc/test_fork_choice_reorgs.py @@ -2,10 +2,10 @@ import pytest from consensus_testing import ( + AggregatedAttestationSpec, BlockSpec, BlockStep, ForkChoiceTestFiller, - SignedAttestationSpec, StoreChecks, TickStep, generate_pre_state, @@ -862,42 +862,20 @@ def test_reorg_on_newly_justified_slot( parent_label="fork_b_1", label="fork_b_2", attestations=[ - SignedAttestationSpec( - validator_id=Uint64(0), - slot=Slot(5), - target_slot=Slot(5), - target_root_label="fork_b_1", - ), - SignedAttestationSpec( - validator_id=Uint64(1), - slot=Slot(5), - target_slot=Slot(5), - target_root_label="fork_b_1", - ), + # Aggregated attestation from validators 0, 1, 5, 6, 7, 8 # fork_b_1 should be able to justify without extra attestations # from validator 5 and 6 but the test is failing without these - # two attestations below because block proposer's attestations - # are not being counted towards justification - SignedAttestationSpec( - validator_id=Uint64(5), - slot=Slot(5), - target_slot=Slot(5), - target_root_label="fork_b_1", - ), - SignedAttestationSpec( - validator_id=Uint64(6), - slot=Slot(5), - target_slot=Slot(5), - target_root_label="fork_b_1", - ), - SignedAttestationSpec( - validator_id=Uint64(7), - slot=Slot(5), - target_slot=Slot(5), - target_root_label="fork_b_1", - ), - SignedAttestationSpec( - validator_id=Uint64(8), + # because block proposer's attestations are not being counted + # towards justification + AggregatedAttestationSpec( + validator_ids=[ + Uint64(0), + Uint64(1), + Uint64(5), + Uint64(6), + Uint64(7), + Uint64(8), + ], slot=Slot(5), target_slot=Slot(5), target_root_label="fork_b_1", diff --git a/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py b/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py index 2c2df749..5c6c8ef1 100644 --- a/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py +++ b/tests/consensus/devnet/verify_signatures/test_invalid_signatures.py @@ -2,17 +2,19 @@ import pytest from consensus_testing import ( + AggregatedAttestationSpec, BlockSpec, VerifySignaturesTestFiller, generate_pre_state, ) from lean_spec.subspecs.containers.slot import Slot +from lean_spec.types import Uint64 pytestmark = pytest.mark.valid_until("Devnet") -def test_invalid_signature( +def test_invalid_proposer_signature( verify_signatures_test: VerifySignaturesTestFiller, ) -> None: """ @@ -46,54 +48,53 @@ def test_invalid_signature( ) -# TODO: Add test for mixed valid and invalid signatures -# This test currently fails because attester-signature verification relies on the -# aggregated multisig proof, but multisig aggregation/verification runs in test_mode. -# Since the proposer signature is valid and verified individually, the block is not rejected.” -# def test_mixed_valid_invalid_signatures( -# verify_signatures_test: VerifySignaturesTestFiller, -# ) -> None: -# """ -# Test that signature verification catches invalid signatures among valid ones. +def test_invalid_aggregated_attestation_signature( + verify_signatures_test: VerifySignaturesTestFiller, +) -> None: + """ + Test that invalid aggregated attestation signatures are properly rejected. -# 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 + Scenario + -------- + - Single block at slot 1 + - Proposer attestation from validator 1 (valid) + - Two aggregated attestations with different data: + - One from validator 0 with valid signature + - One from validator 2 with invalid signature -# Expected Behavior -# ----------------- -# 1. The SignedBlockWithAttestation is rejected due to 1 invalid signature + Expected Behavior + ----------------- + 1. The SignedBlockWithAttestation is rejected due to invalid aggregated 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), -# 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, -# ) + Why This Matters + ---------------- + This test verifies that aggregated signature verification: + - Properly validates leanVM aggregated proofs for each attestation group + - Rejects blocks containing any invalid aggregated attestation signature + - Works correctly even when some attestations have valid signatures + """ + verify_signatures_test( + anchor_state=generate_pre_state(num_validators=3), + block=BlockSpec( + slot=Slot(2), + attestations=[ + # Valid aggregated attestation + AggregatedAttestationSpec( + validator_ids=[Uint64(0)], + slot=Slot(2), + target_slot=Slot(1), + target_root_label="genesis", + valid_signature=True, + ), + # Invalid aggregated attestation (different target to force separate aggregation) + AggregatedAttestationSpec( + validator_ids=[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 index e9b01298..f7601eb1 100644 --- a/tests/consensus/devnet/verify_signatures/test_valid_signatures.py +++ b/tests/consensus/devnet/verify_signatures/test_valid_signatures.py @@ -2,8 +2,8 @@ import pytest from consensus_testing import ( + AggregatedAttestationSpec, BlockSpec, - SignedAttestationSpec, VerifySignaturesTestFiller, generate_pre_state, ) @@ -55,36 +55,30 @@ def test_proposer_and_attester_signatures( -------- - 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 + - Aggregated attestation from validators 0 and 2 (in addition to proposer) + - Verifies that all signatures are generated and aggregated 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 + 2. Aggregated attestation signatures can be verified against the validators' + pubkeys in the state Why This Matters ---------------- - This test verifies multi-validator signature scenarios: + This test verifies multi-validator signature aggregation: - 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) + - Attestations with same data are properly aggregated + - leanVM signature aggregation works with multiple validators """ verify_signatures_test( anchor_state=generate_pre_state(num_validators=3), 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), + AggregatedAttestationSpec( + validator_ids=[Uint64(0), Uint64(2)], slot=Slot(1), target_slot=Slot(0), target_root_label="genesis",