From 6d49ce61b9642774896c9eaeecd553d65fcb2f50 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:47:42 +0200 Subject: [PATCH 1/2] refactor(testing): unify the three attestation spec files into one module The three attestation spec files declared the same checkpoint fields and the checkpoint-resolution bodies in the two aggregated variants were byte-for-byte identical (~45 duplicated lines plus 13 parallel field declarations each). Changes: - one attestation_specs module replaces the three files - a shared AttestationSpec base declares the common checkpoint fields and resolves target, head, and source once; the source default is a caller-provided checkpoint, since the aggregated paths fall back to the latest justified checkpoint while the single-validator gossip path falls back to the anchor block - AggregatedAttestationSpec absorbs GossipAggregatedAttestationSpec: the two classes were field-identical, so the merged class carries both builders (signed gossip aggregation and invalid block-embedded proof); all call sites switch to the merged name - GossipAttestationSpec keeps its single-validator semantics but inherits the shared resolution; its target_root_override, head_root_override, and source_root_override fields become target_root, head_root, and source_root, matching the aggregated naming with the same root-takes-priority cascade Every existing test scenario resolves to identical attestation data: the three tests using raw root overrides each set one override next to a label whose slot already matches, so the priority change is unobservable in emitted vectors. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../testing/src/consensus_testing/__init__.py | 2 - .../test_fixtures/state_transition.py | 4 +- .../consensus_testing/test_types/__init__.py | 10 +- .../test_types/aggregated_attestation_spec.py | 217 ---------- .../test_types/attestation_specs.py | 373 ++++++++++++++++++ .../test_types/block_spec.py | 8 +- .../gossip_aggregated_attestation_spec.py | 209 ---------- .../test_types/gossip_attestation_spec.py | 215 ---------- .../test_types/step_types.py | 12 +- .../lstar/fc/test_block_production.py | 13 +- tests/consensus/lstar/fc/test_equivocation.py | 5 +- ...ossip_aggregated_attestation_validation.py | 20 +- .../fc/test_gossip_attestation_validation.py | 6 +- tests/consensus/lstar/fc/test_safe_target.py | 19 +- .../consensus/lstar/fc/test_store_pruning.py | 25 +- tests/consensus/lstar/fc/test_tick_system.py | 10 +- 16 files changed, 437 insertions(+), 711 deletions(-) delete mode 100644 packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py create mode 100644 packages/testing/src/consensus_testing/test_types/attestation_specs.py delete mode 100644 packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py delete mode 100644 packages/testing/src/consensus_testing/test_types/gossip_attestation_spec.py diff --git a/packages/testing/src/consensus_testing/__init__.py b/packages/testing/src/consensus_testing/__init__.py index b0c31f99c..5df38efa2 100644 --- a/packages/testing/src/consensus_testing/__init__.py +++ b/packages/testing/src/consensus_testing/__init__.py @@ -37,7 +37,6 @@ BlockSpec, BlockStep, ForkChoiceStep, - GossipAggregatedAttestationSpec, GossipAggregatedAttestationStep, GossipAttestationSpec, StateExpectation, @@ -62,7 +61,6 @@ __all__ = [ # Public API "AggregatedAttestationSpec", - "GossipAggregatedAttestationSpec", "GossipAttestationSpec", "BlockSpec", "forks", 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 5748734e9..d41d7c7d1 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -278,7 +278,7 @@ def _build_block_from_spec( forced = [ AggregatedAttestation( aggregation_bits=AggregationBits.from_indices(fa.validator_indices), - data=fa.build_attestation_data(block_registry, state), + data=fa.build_attestation_data(block_registry, state.latest_justified), ) for fa in spec.forced_attestations ] @@ -334,7 +334,7 @@ def _build_aggregated_payloads_from_spec( if spec.signer_ids is not None: raise NotImplementedError("signer_ids not yet supported in StateTransitionTest") - attestation_data = spec.build_attestation_data(block_registry, state) + attestation_data = spec.build_attestation_data(block_registry, state.latest_justified) proof = key_manager.sign_and_aggregate(spec.validator_indices, attestation_data) payloads.setdefault(attestation_data, set()).add(proof) diff --git a/packages/testing/src/consensus_testing/test_types/__init__.py b/packages/testing/src/consensus_testing/test_types/__init__.py index 41007fb84..2b3c02669 100644 --- a/packages/testing/src/consensus_testing/test_types/__init__.py +++ b/packages/testing/src/consensus_testing/test_types/__init__.py @@ -1,11 +1,10 @@ """Test types for consensus test fixtures.""" -from consensus_testing.test_types.aggregated_attestation_spec import AggregatedAttestationSpec -from consensus_testing.test_types.block_spec import BlockSpec -from consensus_testing.test_types.gossip_aggregated_attestation_spec import ( - GossipAggregatedAttestationSpec, +from consensus_testing.test_types.attestation_specs import ( + AggregatedAttestationSpec, + GossipAttestationSpec, ) -from consensus_testing.test_types.gossip_attestation_spec import GossipAttestationSpec +from consensus_testing.test_types.block_spec import BlockSpec from consensus_testing.test_types.state_expectation import StateExpectation from consensus_testing.test_types.step_types import ( AttestationStep, @@ -24,7 +23,6 @@ __all__ = [ "AggregatedAttestationSpec", "GossipAttestationSpec", - "GossipAggregatedAttestationSpec", "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 deleted file mode 100644 index b8888241d..000000000 --- a/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py +++ /dev/null @@ -1,217 +0,0 @@ -"""Lightweight aggregated attestation specification for test definitions.""" - -from __future__ import annotations - -from consensus_testing.keys import XmssKeyManager -from consensus_testing.test_types.utils import resolve_checkpoint -from lean_spec.base import CamelModel -from lean_spec.spec.forks import AggregationBits, Checkpoint, Slot, ValidatorIndex -from lean_spec.spec.forks.lstar.containers import ( - AggregatedAttestation, - AggregatedAttestations, - AttestationData, - Block, - SingleMessageAggregate, - State, -) -from lean_spec.spec.ssz import ByteList512KiB, Bytes32 - - -class AggregatedAttestationSpec(CamelModel): - """ - Aggregated attestation specification for test definitions. - - Specifies multiple validators attesting to the same data. - Head is derived from target. - Source defaults to the latest justified checkpoint unless overridden. - """ - - validator_indices: list[ValidatorIndex] - """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 | None = None - """ - Label referencing a previously created block as the target. - - The block must exist in the block registry with this label. - """ - - target_root: Bytes32 | None = None - """Optional explicit target root (bypasses label lookup).""" - - head_root_label: str | None = None - """Optional label for the head checkpoint.""" - - head_root: Bytes32 | None = None - """Optional explicit head root.""" - - head_slot: Slot | None = None - """Optional override for the head checkpoint slot.""" - - source_root_label: str | None = None - """Optional label for the source checkpoint.""" - - source_root: Bytes32 | None = None - """Optional explicit source root.""" - - source_slot: Slot | None = None - """Optional override for the source checkpoint slot.""" - - 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). - """ - - signer_ids: list[ValidatorIndex] | None = None - """ - Override which validators actually sign the attestation. - - When None (default), signatures are generated using the validators in validator_indices. - When specified, signatures are generated using these validator indices instead. - - This creates a mismatch between claimed participants and actual signers. - Useful for testing that verification rejects attestations where valid signatures - don't correspond to the claimed validators. - - Must have same length as validator_indices when specified. - """ - - def build_attestation_data( - self, - block_registry: dict[str, Block], - state: State, - ) -> AttestationData: - """ - Build attestation data from this specification. - - Args: - block_registry: Labeled blocks for target resolution. - state: State for source checkpoint lookup. - - Returns: - Attestation data shared by all validators in the aggregation. - - Raises: - ValueError: If no target root source is provided. - """ - if self.target_root is not None: - target = Checkpoint(root=self.target_root, slot=self.target_slot) - elif self.target_root_label is not None: - target = resolve_checkpoint(self.target_root_label, self.target_slot, block_registry) - else: - raise ValueError("aggregated attestation spec requires a target root") - - if self.head_root is not None: - head = Checkpoint( - root=self.head_root, - slot=self.head_slot if self.head_slot is not None else self.target_slot, - ) - elif self.head_root_label is not None: - head = resolve_checkpoint(self.head_root_label, self.head_slot, block_registry) - else: - head = Checkpoint( - root=target.root, - slot=self.head_slot if self.head_slot is not None else target.slot, - ) - - if self.source_root is not None: - source = Checkpoint( - root=self.source_root, - slot=( - self.source_slot - if self.source_slot is not None - else state.latest_justified.slot - ), - ) - elif self.source_root_label is not None: - source = resolve_checkpoint(self.source_root_label, self.source_slot, block_registry) - else: - source = Checkpoint( - root=state.latest_justified.root, - slot=( - self.source_slot - if self.source_slot is not None - else state.latest_justified.slot - ), - ) - - return AttestationData( - slot=self.slot, - head=head, - target=target, - source=source, - ) - - def build_invalid_proof( - self, - block_registry: dict[str, Block], - state: State, - key_manager: XmssKeyManager, - block: Block, - ) -> tuple[Block, SingleMessageAggregate]: - """ - Build an invalid attestation proof and append it to the block body. - - Handles two invalidity scenarios: - - - Invalid signature: correct participant bitfield, zeroed-out proof bytes - - Signer mismatch: valid proof from wrong validators, claimed participants differ - - Args: - block_registry: Labeled blocks for checkpoint resolution. - state: State for attestation data building. - key_manager: XMSS key manager for signing (mismatch scenario). - block: Current block to append the invalid attestation to. - - Returns: - Tuple of (updated block with appended attestation, invalid proof). - """ - attestation_data = self.build_attestation_data(block_registry, state) - - aggregation_bits = AggregationBits.from_indices(self.validator_indices) - invalid_aggregated = AggregatedAttestation( - aggregation_bits=aggregation_bits, - data=attestation_data, - ) - - # Empty proof bytes flag "no real single-message aggregate here" — the caller treats - # any such entry as a placeholder and bypasses real binding merges. - placeholder = ByteList512KiB(data=b"") - - if not self.valid_signature: - invalid_proof = SingleMessageAggregate(participants=aggregation_bits, proof=placeholder) - elif self.signer_ids is not None: - # Valid proof from wrong validators (participant mismatch). - valid_proof = key_manager.sign_and_aggregate(self.signer_ids, attestation_data) - invalid_proof = SingleMessageAggregate( - participants=aggregation_bits, proof=valid_proof.proof - ) - else: - invalid_proof = SingleMessageAggregate(participants=aggregation_bits, proof=placeholder) - - # Append invalid attestation to the block body. - block = block.model_copy( - update={ - "body": block.body.model_copy( - update={ - "attestations": AggregatedAttestations( - data=[*block.body.attestations.data, invalid_aggregated] - ) - } - ) - } - ) - - return block, invalid_proof diff --git a/packages/testing/src/consensus_testing/test_types/attestation_specs.py b/packages/testing/src/consensus_testing/test_types/attestation_specs.py new file mode 100644 index 000000000..76859b01a --- /dev/null +++ b/packages/testing/src/consensus_testing/test_types/attestation_specs.py @@ -0,0 +1,373 @@ +"""Lightweight attestation specifications for test definitions.""" + +from __future__ import annotations + +from consensus_testing.keys import XmssKeyManager, create_dummy_signature +from consensus_testing.test_types.utils import resolve_checkpoint +from lean_spec.base import CamelModel +from lean_spec.spec.crypto.merkleization import hash_tree_root +from lean_spec.spec.forks import AggregationBits, Checkpoint, Slot, ValidatorIndex +from lean_spec.spec.forks.lstar.containers import ( + AggregatedAttestation, + AggregatedAttestations, + AttestationData, + Block, + SignedAggregatedAttestation, + SignedAttestation, + SingleMessageAggregate, + State, + Store, +) +from lean_spec.spec.forks.lstar.spec import LstarSpec +from lean_spec.spec.ssz import ByteList512KiB, Bytes32 + + +class AttestationSpec(CamelModel): + """ + Shared core for attestation specifications. + + Declares the checkpoint fields (target, head, source) common to all + attestation specs and resolves them into attestation data. + """ + + 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 | None = None + """ + Label referencing a previously created block as the target. + + The block must exist in the block registry with this label. + """ + + target_root: Bytes32 | None = None + """Optional explicit target root (bypasses label lookup).""" + + head_root_label: str | None = None + """Optional label for the head checkpoint.""" + + head_root: Bytes32 | None = None + """Optional explicit head root.""" + + head_slot: Slot | None = None + """Optional override for the head checkpoint slot.""" + + source_root_label: str | None = None + """Optional label for the source checkpoint.""" + + source_root: Bytes32 | None = None + """Optional explicit source root.""" + + source_slot: Slot | None = None + """Optional override for the source checkpoint slot.""" + + 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 instead of proper XMSS signatures. + + Defaults to True (valid signatures). + """ + + def build_attestation_data( + self, + block_registry: dict[str, Block], + default_source: Checkpoint, + ) -> AttestationData: + """ + Build attestation data by resolving the three checkpoints (target, head, source). + + Args: + block_registry: Labeled blocks for checkpoint resolution. + default_source: Source checkpoint used when neither an explicit + root nor a label overrides the source. + + Returns: + Attestation data with all three checkpoints resolved. + + Raises: + ValueError: If the target has neither an explicit root nor a label. + """ + # Resolve the target checkpoint. + # + # An explicit root takes highest priority. + # A label triggers lookup in the block registry. + # Unlike the other two checkpoints, the target is mandatory. + if self.target_root is not None: + target = Checkpoint(root=self.target_root, slot=self.target_slot) + elif self.target_root_label is not None: + target = resolve_checkpoint(self.target_root_label, self.target_slot, block_registry) + else: + raise ValueError("attestation spec requires a target root") + + # Resolve the head checkpoint. + # + # Priority: explicit root > label > target checkpoint. + # - When using an explicit root without a slot, the target slot is used. + # - When no head information is provided at all, the head mirrors the target. + # + # This matches honest validator behavior. + if self.head_root is not None: + head = Checkpoint( + root=self.head_root, + slot=self.head_slot if self.head_slot is not None else self.target_slot, + ) + elif self.head_root_label is not None: + head = resolve_checkpoint(self.head_root_label, self.head_slot, block_registry) + else: + head = Checkpoint( + root=target.root, + slot=self.head_slot if self.head_slot is not None else target.slot, + ) + + # Resolve the source checkpoint. + # + # Priority: explicit root > label > the provided default. + # The source represents the most recent justified checkpoint the attester is aware of. + # When not overridden, the caller-provided default supplies the correct value. + if self.source_root is not None: + source = Checkpoint( + root=self.source_root, + slot=self.source_slot if self.source_slot is not None else default_source.slot, + ) + elif self.source_root_label is not None: + source = resolve_checkpoint(self.source_root_label, self.source_slot, block_registry) + else: + source = Checkpoint( + root=default_source.root, + slot=self.source_slot if self.source_slot is not None else default_source.slot, + ) + + return AttestationData( + slot=self.slot, + head=head, + target=target, + source=source, + ) + + +class AggregatedAttestationSpec(AttestationSpec): + """ + Aggregated attestation specification for test definitions. + + Specifies multiple validators attesting to the same data. + Used both for attestations embedded in blocks and for + aggregations received via gossip. + The source defaults to the latest justified checkpoint unless overridden. + """ + + validator_indices: list[ValidatorIndex] + """The indices of validators making the attestation (required).""" + + signer_ids: list[ValidatorIndex] | None = None + """ + Override which validators actually sign the attestation. + + When None (default), signatures are generated using the validators in validator_indices. + When specified, signatures are generated using these validator indices instead. + + This creates a mismatch between claimed participants and actual signers. + Useful for testing that verification rejects attestations where valid signatures + don't correspond to the claimed validators. + + Must have same length as validator_indices when specified. + """ + + def build_signed( + self, + block_registry: dict[str, Block], + state: State, + key_manager: XmssKeyManager, + ) -> SignedAggregatedAttestation: + """ + Build a complete signed aggregated attestation from this specification. + + Supports valid, invalid, and participant-mismatch scenarios + depending on the spec's signature and signer configuration. + + Args: + block_registry: Labeled blocks for checkpoint resolution. + state: Head state providing the latest justified checkpoint as source fallback. + key_manager: XMSS key manager for signing attestation data. + + Returns: + Signed aggregated attestation ready for gossip processing. + """ + attestation_data = self.build_attestation_data(block_registry, state.latest_justified) + + # Separate "claimed" from "actual" participants. + # + # - Claimed validators appear in the proof's participant bitfield. + # - Actual signers produce the cryptographic material. + # They default to the same set for honest attestations. + validator_indices = self.validator_indices + signer_ids = self.signer_ids or self.validator_indices + + # Path 1: Invalid signature. + # + # Correct participant bitfield but zeroed-out proof bytes. + # Exercises signature verification rejection. + if not self.valid_signature: + placeholder = ByteList512KiB(data=b"\x00" * 32) + proof = SingleMessageAggregate( + participants=AggregationBits.from_indices(validator_indices), + proof=placeholder, + ) + return SignedAggregatedAttestation(data=attestation_data, proof=proof) + + # Path 2: Valid signature. + proof = key_manager.sign_and_aggregate(signer_ids, attestation_data) + + # Path 3: Participant mismatch. + # + # Replace the participant bitfield with different validator indices + # while keeping the original proof bytes intact. + # The proof is cryptographically valid for the actual signers, + # but the claimed participants no longer match. + # The store must detect and reject this inconsistency. + if self.signer_ids and self.signer_ids != self.validator_indices: + proof = SingleMessageAggregate( + participants=AggregationBits.from_indices(validator_indices), + proof=proof.proof, + ) + + return SignedAggregatedAttestation(data=attestation_data, proof=proof) + + def build_invalid_proof( + self, + block_registry: dict[str, Block], + state: State, + key_manager: XmssKeyManager, + block: Block, + ) -> tuple[Block, SingleMessageAggregate]: + """ + Build an invalid attestation proof and append it to the block body. + + Handles two invalidity scenarios: + + - Invalid signature: correct participant bitfield, zeroed-out proof bytes + - Signer mismatch: valid proof from wrong validators, claimed participants differ + + Args: + block_registry: Labeled blocks for checkpoint resolution. + state: State for attestation data building. + key_manager: XMSS key manager for signing (mismatch scenario). + block: Current block to append the invalid attestation to. + + Returns: + Tuple of (updated block with appended attestation, invalid proof). + """ + attestation_data = self.build_attestation_data(block_registry, state.latest_justified) + + aggregation_bits = AggregationBits.from_indices(self.validator_indices) + invalid_aggregated = AggregatedAttestation( + aggregation_bits=aggregation_bits, + data=attestation_data, + ) + + # Empty proof bytes flag "no real single-message aggregate here" — the caller treats + # any such entry as a placeholder and bypasses real binding merges. + placeholder = ByteList512KiB(data=b"") + + if not self.valid_signature: + invalid_proof = SingleMessageAggregate(participants=aggregation_bits, proof=placeholder) + elif self.signer_ids is not None: + # Valid proof from wrong validators (participant mismatch). + valid_proof = key_manager.sign_and_aggregate(self.signer_ids, attestation_data) + invalid_proof = SingleMessageAggregate( + participants=aggregation_bits, proof=valid_proof.proof + ) + else: + invalid_proof = SingleMessageAggregate(participants=aggregation_bits, proof=placeholder) + + # Append invalid attestation to the block body. + block = block.model_copy( + update={ + "body": block.body.model_copy( + update={ + "attestations": AggregatedAttestations( + data=[*block.body.attestations.data, invalid_aggregated] + ) + } + ) + } + ) + + return block, invalid_proof + + +class GossipAttestationSpec(AttestationSpec): + """ + Gossip attestation specification for test definitions. + + Specifies a single validator attesting via the gossip network. + The source defaults to the anchor (genesis) block instead of the + latest justified checkpoint. + Honest attestations without overrides use data produced by the Store. + """ + + validator_index: ValidatorIndex + """The index of the validator making the attestation (required).""" + + @property + def has_overrides(self) -> bool: + """Whether any checkpoint field is explicitly overridden.""" + return ( + self.head_root_label is not None + or self.head_slot is not None + or self.source_root_label is not None + or self.source_slot is not None + or self.target_root is not None + or self.head_root is not None + or self.source_root is not None + ) + + def build_signed( + self, + block_registry: dict[str, Block], + key_manager: XmssKeyManager, + store: Store, + anchor_block: Block, + expected_valid: bool, + ) -> SignedAttestation: + """ + Build a complete signed attestation from this specification. + + Valid attestations without overrides use honest data from the Store. + Invalid or overridden attestations use this spec's checkpoint fields, + with the anchor block as the default source. + + Args: + block_registry: Labeled blocks for target resolution. + key_manager: XMSS key manager for signing. + store: Fork choice store for honest attestation data production. + anchor_block: Genesis/anchor block for source checkpoint default. + expected_valid: Whether the step expects this attestation to succeed. + + Returns: + Signed attestation ready for gossip processing. + """ + if not expected_valid or self.has_overrides: + anchor_source = Checkpoint(root=hash_tree_root(anchor_block), slot=anchor_block.slot) + attestation_data = self.build_attestation_data(block_registry, anchor_source) + else: + # Honest path: use the Store's own attestation data production. + attestation_data = LstarSpec().produce_attestation_data(store, self.slot) + + signature = ( + key_manager.sign_attestation_data(self.validator_index, attestation_data) + if self.valid_signature + else create_dummy_signature() + ) + + return SignedAttestation( + validator_index=self.validator_index, + data=attestation_data, + 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 f21c1ca85..eb4e672d4 100644 --- a/packages/testing/src/consensus_testing/test_types/block_spec.py +++ b/packages/testing/src/consensus_testing/test_types/block_spec.py @@ -6,7 +6,7 @@ from collections import defaultdict from consensus_testing.keys import XmssKeyManager, create_dummy_signature -from consensus_testing.test_types.aggregated_attestation_spec import AggregatedAttestationSpec +from consensus_testing.test_types.attestation_specs import AggregatedAttestationSpec from lean_spec.base import CamelModel from lean_spec.spec.crypto.merkleization import hash_tree_root from lean_spec.spec.crypto.xmss.containers import Signature @@ -204,7 +204,9 @@ def build_attestations( for aggregated_spec in self.attestations: # Build attestation data once. # All validators in this aggregation vote for the same target. - attestation_data = aggregated_spec.build_attestation_data(block_registry, state) + attestation_data = aggregated_spec.build_attestation_data( + block_registry, state.latest_justified + ) # Create one attestation per validator. # Each validator signs independently; signatures aggregate later. @@ -546,7 +548,7 @@ def build_signed_block_with_store( if self.forced_attestations: for attestation_spec in self.forced_attestations: attestation_data = attestation_spec.build_attestation_data( - block_registry, parent_state + block_registry, parent_state.latest_justified ) proof = key_manager.sign_and_aggregate( attestation_spec.validator_indices, attestation_data diff --git a/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py deleted file mode 100644 index 05af1c6da..000000000 --- a/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py +++ /dev/null @@ -1,209 +0,0 @@ -"""Lightweight aggregated-gossip attestation specification.""" - -from __future__ import annotations - -from consensus_testing.keys import XmssKeyManager -from consensus_testing.test_types.utils import resolve_checkpoint -from lean_spec.base import CamelModel -from lean_spec.spec.forks import AggregationBits, Checkpoint, Slot, ValidatorIndex -from lean_spec.spec.forks.lstar.containers import ( - AttestationData, - Block, - SignedAggregatedAttestation, - SingleMessageAggregate, - State, -) -from lean_spec.spec.ssz import ByteList512KiB, Bytes32 - - -class GossipAggregatedAttestationSpec(CamelModel): - """ - Specification for an aggregated attestation received via gossip. - - The spec allows overriding head/source checkpoints to exercise validation logic. - """ - - validator_indices: list[ValidatorIndex] - """Claimed validators participating in the aggregation.""" - - slot: Slot - """Slot of the attestation.""" - - target_slot: Slot - """Slot of the attestation target checkpoint.""" - - target_root_label: str | None = None - """Label referencing the target block root.""" - - target_root: Bytes32 | None = None - """Optional explicit target root (bypasses label lookup).""" - - head_root_label: str | None = None - """Optional label for the head checkpoint.""" - - head_root: Bytes32 | None = None - """Optional explicit head root.""" - - head_slot: Slot | None = None - """Optional override for the head checkpoint slot.""" - - source_root_label: str | None = None - """Optional label for the source checkpoint.""" - - source_root: Bytes32 | None = None - """Optional explicit source root.""" - - source_slot: Slot | None = None - """Optional override for the source checkpoint slot.""" - - valid_signature: bool = True - """Whether the aggregated proof should be generated with valid signatures.""" - - signer_ids: list[ValidatorIndex] | None = None - """Optional override for which validators actually produce the signatures.""" - - def build_attestation_data( - self, - block_registry: dict[str, Block], - state: State, - ) -> AttestationData: - """ - Build attestation data by resolving the three checkpoints (target, head, source). - - The state provides fallback values for the source checkpoint - when neither an explicit root nor a label is specified. - - Args: - block_registry: Labeled blocks for checkpoint resolution. - state: Head state providing the latest justified checkpoint as source fallback. - - Returns: - Attestation data with all three checkpoints resolved. - - Raises: - ValueError: If the target has neither an explicit root nor a label. - """ - # Resolve the target checkpoint. - # - # An explicit root takes highest priority. - # A label triggers lookup in the block registry. - # Unlike the other two checkpoints, the target is mandatory. - if self.target_root is not None: - target = Checkpoint(root=self.target_root, slot=self.target_slot) - elif self.target_root_label is not None: - target = resolve_checkpoint(self.target_root_label, self.target_slot, block_registry) - else: - raise ValueError("gossip aggregated attestation spec requires a target root") - - # Resolve the head checkpoint. - # - # Priority: explicit root > label > target checkpoint. - # - When using an explicit root without a slot, the target slot is used. - # - When no head information is provided at all, the head mirrors the target. - # - # This matches honest validator behavior. - if self.head_root is not None: - head = Checkpoint( - root=self.head_root, - slot=self.head_slot if self.head_slot is not None else self.target_slot, - ) - elif self.head_root_label is not None: - head = resolve_checkpoint(self.head_root_label, self.head_slot, block_registry) - else: - head = Checkpoint( - root=target.root, - slot=self.head_slot if self.head_slot is not None else target.slot, - ) - - # Resolve the source checkpoint. - # - # Priority: explicit root > label > latest justified from state. - # The source represents the most recent justified checkpoint the attester is aware of. - # When not overridden, the state's latest justified checkpoint provides the correct default. - if self.source_root is not None: - source = Checkpoint( - root=self.source_root, - slot=( - self.source_slot - if self.source_slot is not None - else state.latest_justified.slot - ), - ) - elif self.source_root_label is not None: - source = resolve_checkpoint(self.source_root_label, self.source_slot, block_registry) - else: - source = Checkpoint( - root=state.latest_justified.root, - slot=( - self.source_slot - if self.source_slot is not None - else state.latest_justified.slot - ), - ) - - return AttestationData( - slot=self.slot, - head=head, - target=target, - source=source, - ) - - def build_signed( - self, - block_registry: dict[str, Block], - state: State, - key_manager: XmssKeyManager, - ) -> SignedAggregatedAttestation: - """ - Build a complete signed aggregated attestation from this specification. - - Supports valid, invalid, and participant-mismatch scenarios - depending on the spec's signature and signer configuration. - - Args: - block_registry: Labeled blocks for checkpoint resolution. - state: Head state for checkpoint resolution fallbacks. - key_manager: XMSS key manager for signing attestation data. - - Returns: - Signed aggregated attestation ready for gossip processing. - """ - attestation_data = self.build_attestation_data(block_registry, state) - - # Separate "claimed" from "actual" participants. - # - # - Claimed validators appear in the proof's participant bitfield. - # - Actual signers produce the cryptographic material. - # They default to the same set for honest attestations. - validator_indices = self.validator_indices - signer_ids = self.signer_ids or self.validator_indices - - # Path 1: Invalid signature. - # - # Correct participant bitfield but zeroed-out proof bytes. - # Exercises signature verification rejection. - if not self.valid_signature: - placeholder = ByteList512KiB(data=b"\x00" * 32) - proof = SingleMessageAggregate( - participants=AggregationBits.from_indices(validator_indices), - proof=placeholder, - ) - return SignedAggregatedAttestation(data=attestation_data, proof=proof) - - # Path 2: Valid signature. - proof = key_manager.sign_and_aggregate(signer_ids, attestation_data) - - # Path 3: Participant mismatch. - # - # Replace the participant bitfield with different validator indices - # while keeping the original proof bytes intact. - # The proof is cryptographically valid for the actual signers, - # but the claimed participants no longer match. - # The store must detect and reject this inconsistency. - if self.signer_ids and self.signer_ids != self.validator_indices: - proof = SingleMessageAggregate( - participants=AggregationBits.from_indices(validator_indices), - proof=proof.proof, - ) - - return SignedAggregatedAttestation(data=attestation_data, proof=proof) diff --git a/packages/testing/src/consensus_testing/test_types/gossip_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/gossip_attestation_spec.py deleted file mode 100644 index 68af09b07..000000000 --- a/packages/testing/src/consensus_testing/test_types/gossip_attestation_spec.py +++ /dev/null @@ -1,215 +0,0 @@ -"""Lightweight gossip attestation specification for test definitions.""" - -from __future__ import annotations - -from consensus_testing.keys import XmssKeyManager, create_dummy_signature -from consensus_testing.test_types.utils import resolve_checkpoint -from lean_spec.base import CamelModel -from lean_spec.spec.crypto.merkleization import hash_tree_root -from lean_spec.spec.forks import Checkpoint, Slot, ValidatorIndex -from lean_spec.spec.forks.lstar.containers import AttestationData, Block, SignedAttestation, Store -from lean_spec.spec.forks.lstar.spec import LstarSpec -from lean_spec.spec.ssz import Bytes32 - - -class GossipAttestationSpec(CamelModel): - """ - Gossip attestation specification for test definitions. - - Specifies a single validator attesting via gossip network. - Similar to AggregatedAttestationSpec but for individual gossip attestations. - """ - - validator_index: ValidatorIndex - """The index of the validator 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. - """ - - head_root_label: str | None = None - """ - Label referencing a previously created block as the head. - - When None (default), head is set equal to the target checkpoint. - When specified, resolves to a different block for testing topology violations. - """ - - head_slot: Slot | None = None - """ - Override for the head checkpoint slot. - - When None (default), uses the actual slot of the head block. - When specified, creates a mismatch for testing consistency checks. - """ - - source_root_label: str | None = None - """ - Label referencing a previously created block as the source. - - When None (default), source is the anchor (genesis) block. - When specified, resolves to a different block for testing source overrides. - """ - - source_slot: Slot | None = None - """ - Override for the source checkpoint slot. - - When None (default), uses the actual slot of the source block. - When specified, creates a mismatch for testing consistency checks. - """ - - target_root_override: Bytes32 | None = None - """ - Raw root override for the target checkpoint. - - Bypasses label resolution. Used to inject roots not in the store - for testing unknown block rejection. - """ - - head_root_override: Bytes32 | None = None - """ - Raw root override for the head checkpoint. - - Bypasses label resolution. Used to inject roots not in the store - for testing unknown block rejection. - """ - - source_root_override: Bytes32 | None = None - """ - Raw root override for the source checkpoint. - - Bypasses label resolution. Used to inject roots not in the store - for testing unknown block rejection. - """ - - 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 instead of a proper XMSS signature. - - Defaults to True (valid signature). - """ - - @property - def has_overrides(self) -> bool: - """Whether any checkpoint field is explicitly overridden.""" - return ( - self.head_root_label is not None - or self.head_slot is not None - or self.source_root_label is not None - or self.source_slot is not None - or self.target_root_override is not None - or self.head_root_override is not None - or self.source_root_override is not None - ) - - def build_attestation_data( - self, - block_registry: dict[str, Block], - anchor_block: Block, - ) -> AttestationData: - """ - Build attestation data with explicit checkpoint overrides. - - Used for invalid or non-standard attestations where the test - intentionally creates mismatches for validation testing. - - Args: - block_registry: Labeled blocks for checkpoint resolution. - anchor_block: Genesis/anchor block used as default source. - - Returns: - Attestation data with overridden checkpoints. - """ - target = resolve_checkpoint(self.target_root_label, self.target_slot, block_registry) - - # Resolve head checkpoint. - # Defaults to the target checkpoint when not overridden. - if self.head_root_label is not None: - head = resolve_checkpoint(self.head_root_label, self.head_slot, block_registry) - else: - head = Checkpoint( - root=target.root, - slot=target.slot if self.head_slot is None else self.head_slot, - ) - - # Resolve source checkpoint. - # Defaults to the anchor (genesis) block when not overridden. - if self.source_root_label is not None: - source = resolve_checkpoint(self.source_root_label, self.source_slot, block_registry) - else: - source = Checkpoint( - root=hash_tree_root(anchor_block), - slot=anchor_block.slot if self.source_slot is None else self.source_slot, - ) - - # Apply raw root overrides. - # These inject roots not in the store for testing unknown block rejection. - if self.target_root_override is not None: - target = Checkpoint(root=self.target_root_override, slot=target.slot) - if self.head_root_override is not None: - head = Checkpoint(root=self.head_root_override, slot=head.slot) - if self.source_root_override is not None: - source = Checkpoint(root=self.source_root_override, slot=source.slot) - - return AttestationData( - slot=self.slot, - head=head, - target=target, - source=source, - ) - - def build_signed( - self, - block_registry: dict[str, Block], - key_manager: XmssKeyManager, - store: Store, - anchor_block: Block, - expected_valid: bool, - ) -> SignedAttestation: - """ - Build a complete signed attestation from this specification. - - Valid attestations without overrides use honest data from the Store. - Invalid or overridden attestations use this spec's checkpoint fields. - - Args: - block_registry: Labeled blocks for target resolution. - key_manager: XMSS key manager for signing. - store: Fork choice store for honest attestation data production. - anchor_block: Genesis/anchor block for source checkpoint default. - expected_valid: Whether the step expects this attestation to succeed. - - Returns: - Signed attestation ready for gossip processing. - """ - if not expected_valid or self.has_overrides: - attestation_data = self.build_attestation_data(block_registry, anchor_block) - else: - # Honest path: use the Store's own attestation data production. - attestation_data = LstarSpec().produce_attestation_data(store, self.slot) - - signature = ( - key_manager.sign_attestation_data(self.validator_index, attestation_data) - if self.valid_signature - else create_dummy_signature() - ) - - return SignedAttestation( - validator_index=self.validator_index, - data=attestation_data, - signature=signature, - ) diff --git a/packages/testing/src/consensus_testing/test_types/step_types.py b/packages/testing/src/consensus_testing/test_types/step_types.py index 222cf2141..eb8a9cf21 100644 --- a/packages/testing/src/consensus_testing/test_types/step_types.py +++ b/packages/testing/src/consensus_testing/test_types/step_types.py @@ -4,11 +4,11 @@ from pydantic import ConfigDict, Field, PrivateAttr, field_serializer, model_validator -from consensus_testing.test_types.block_spec import BlockSpec -from consensus_testing.test_types.gossip_aggregated_attestation_spec import ( - GossipAggregatedAttestationSpec, +from consensus_testing.test_types.attestation_specs import ( + AggregatedAttestationSpec, + GossipAttestationSpec, ) -from consensus_testing.test_types.gossip_attestation_spec import GossipAttestationSpec +from consensus_testing.test_types.block_spec import BlockSpec from consensus_testing.test_types.store_checks import StoreChecks from lean_spec.base import CamelModel from lean_spec.spec.forks.lstar.containers import ( @@ -216,7 +216,7 @@ class GossipAggregatedAttestationStep(BaseForkChoiceStep): step_type: Literal["gossipAggregatedAttestation"] = "gossipAggregatedAttestation" """Discriminator field for serialization.""" - attestation: GossipAggregatedAttestationSpec + attestation: AggregatedAttestationSpec """ Specification for the aggregated gossip attestation. """ @@ -225,7 +225,7 @@ class GossipAggregatedAttestationStep(BaseForkChoiceStep): @field_serializer("attestation", when_used="json") def serialize_gossip_aggregated_attestation( - self, value: GossipAggregatedAttestationSpec + self, value: AggregatedAttestationSpec ) -> dict[str, Any]: """Return the filled aggregated attestation for serialization.""" if self._filled_attestation is None: diff --git a/tests/consensus/lstar/fc/test_block_production.py b/tests/consensus/lstar/fc/test_block_production.py index f0e0e7b92..cd1c306ad 100644 --- a/tests/consensus/lstar/fc/test_block_production.py +++ b/tests/consensus/lstar/fc/test_block_production.py @@ -10,7 +10,6 @@ BlockSpec, BlockStep, ForkChoiceTestFiller, - GossipAggregatedAttestationSpec, GossipAggregatedAttestationStep, StoreChecks, TickStep, @@ -140,7 +139,7 @@ def test_block_builder_fixed_point_advances_justification( # Attestation A: source=1, target=2 # 3/4 validators. Matches justified=1 on the first pass. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -157,7 +156,7 @@ def test_block_builder_fixed_point_advances_justification( # 3/4 validators. Source slot 2 is NOT justified yet. # Only unlocked after A justifies slot 2 on the first pass. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(1), ValidatorIndex(2), @@ -305,7 +304,7 @@ def test_produce_block_enforces_max_attestations_data_limit( # Source auto-resolves to the genesis justified checkpoint. attestation_steps: list[GossipAggregatedAttestationStep] = [ GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=validators, slot=Slot(num_target_blocks), target_slot=Slot(n), @@ -386,7 +385,7 @@ def test_produce_block_includes_pending_attestations( # Validators 1 & 2 gossip an aggregated attestation targeting block_2. # data.slot=2 matches the current slot. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1), ValidatorIndex(2)], slot=Slot(2), target_slot=Slot(2), @@ -548,7 +547,7 @@ def test_block_builder_recovers_finality_after_non_zero_boundary_stall( ], TickStep(time=aggregate_time), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -562,7 +561,7 @@ def test_block_builder_recovers_finality_after_non_zero_boundary_stall( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(1), ValidatorIndex(2), diff --git a/tests/consensus/lstar/fc/test_equivocation.py b/tests/consensus/lstar/fc/test_equivocation.py index 66936730c..e46c2b4f6 100644 --- a/tests/consensus/lstar/fc/test_equivocation.py +++ b/tests/consensus/lstar/fc/test_equivocation.py @@ -8,7 +8,6 @@ BlockSpec, BlockStep, ForkChoiceTestFiller, - GossipAggregatedAttestationSpec, GossipAggregatedAttestationStep, GossipAttestationSpec, StoreChecks, @@ -300,7 +299,7 @@ def test_same_slot_equivocating_attesters_count_once( # Insertion-ordered dicts make this deterministic. # V0 and V1's first votes stick on fork_a. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -312,7 +311,7 @@ def test_same_slot_equivocating_attesters_count_once( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), diff --git a/tests/consensus/lstar/fc/test_gossip_aggregated_attestation_validation.py b/tests/consensus/lstar/fc/test_gossip_aggregated_attestation_validation.py index 9653dcce7..e40bcaa1d 100644 --- a/tests/consensus/lstar/fc/test_gossip_aggregated_attestation_validation.py +++ b/tests/consensus/lstar/fc/test_gossip_aggregated_attestation_validation.py @@ -2,10 +2,10 @@ import pytest from consensus_testing import ( + AggregatedAttestationSpec, BlockSpec, BlockStep, ForkChoiceTestFiller, - GossipAggregatedAttestationSpec, GossipAggregatedAttestationStep, StoreChecks, TickStep, @@ -40,7 +40,7 @@ def test_valid_gossip_aggregated_attestation( checks=StoreChecks(head_slot=Slot(2)), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(2), target_slot=Slot(2), @@ -67,7 +67,7 @@ def test_aggregated_attestation_unknown_source_rejected( checks=StoreChecks(head_slot=Slot(2)), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(2), target_slot=Slot(2), @@ -97,7 +97,7 @@ def test_aggregated_attestation_target_slot_mismatch_rejected( checks=StoreChecks(head_slot=Slot(2)), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(2), target_slot=Slot(3), @@ -125,7 +125,7 @@ def test_aggregated_attestation_head_slot_mismatch_rejected( checks=StoreChecks(head_slot=Slot(2)), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(2), target_slot=Slot(2), @@ -159,7 +159,7 @@ def test_aggregated_attestation_source_after_target_rejected( checks=StoreChecks(head_slot=Slot(3)), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(3), target_slot=Slot(2), @@ -189,7 +189,7 @@ def test_aggregated_attestation_too_far_in_future_rejected( checks=StoreChecks(head_slot=Slot(2)), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(4), target_slot=Slot(2), @@ -230,7 +230,7 @@ def test_aggregated_attestation_at_disparity_boundary_allowed( ), TickStep(interval=SLOT_3_BOUNDARY_INTERVAL), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(3), target_slot=Slot(2), @@ -269,7 +269,7 @@ def test_aggregated_attestation_just_beyond_disparity_boundary_rejected( ), TickStep(interval=SLOT_3_JUST_BEYOND_BOUNDARY_INTERVAL), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(3), target_slot=Slot(2), @@ -312,7 +312,7 @@ def test_aggregated_attestation_one_full_slot_in_future_rejected( checks=StoreChecks(head_slot=Slot(2)), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(1)], slot=Slot(3), target_slot=Slot(2), diff --git a/tests/consensus/lstar/fc/test_gossip_attestation_validation.py b/tests/consensus/lstar/fc/test_gossip_attestation_validation.py index 4f0243a9f..996b04756 100644 --- a/tests/consensus/lstar/fc/test_gossip_attestation_validation.py +++ b/tests/consensus/lstar/fc/test_gossip_attestation_validation.py @@ -702,7 +702,7 @@ def test_attestation_unknown_target_block_rejected( slot=Slot(2), target_slot=Slot(2), target_root_label="block_2", - target_root_override=FAKE_ROOT, + target_root=FAKE_ROOT, valid_signature=False, ), valid=False, @@ -742,7 +742,7 @@ def test_attestation_unknown_head_block_rejected( slot=Slot(2), target_slot=Slot(2), target_root_label="block_2", - head_root_override=FAKE_ROOT, + head_root=FAKE_ROOT, valid_signature=False, ), valid=False, @@ -782,7 +782,7 @@ def test_attestation_unknown_source_block_rejected( slot=Slot(2), target_slot=Slot(2), target_root_label="block_2", - source_root_override=FAKE_ROOT, + source_root=FAKE_ROOT, valid_signature=False, ), valid=False, diff --git a/tests/consensus/lstar/fc/test_safe_target.py b/tests/consensus/lstar/fc/test_safe_target.py index f01e2e563..6fd1b184a 100644 --- a/tests/consensus/lstar/fc/test_safe_target.py +++ b/tests/consensus/lstar/fc/test_safe_target.py @@ -7,7 +7,6 @@ BlockSpec, BlockStep, ForkChoiceTestFiller, - GossipAggregatedAttestationSpec, GossipAggregatedAttestationStep, StoreChecks, TickStep, @@ -98,7 +97,7 @@ def test_safe_target_does_not_advance_below_supermajority( # Here we only care that they arrive in "new" so the safe-target # computation at interval 3 can read them. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(i) for i in range(num_attesters)], slot=Slot(3), target_slot=Slot(2), @@ -184,7 +183,7 @@ def test_safe_target_advances_incrementally_along_the_chain( # Weight accumulates at block_1 only (ancestors of the voted head). TickStep(time=14), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -208,7 +207,7 @@ def test_safe_target_advances_incrementally_along_the_chain( # Weight now reaches block_2 through the new head. TickStep(time=18), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -232,7 +231,7 @@ def test_safe_target_advances_incrementally_along_the_chain( # Full chain now carries weight=3 at every block. TickStep(time=22), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -293,7 +292,7 @@ def test_safe_target_follows_heavier_fork_on_split( TickStep(time=18), # Supermajority (4/6) attests to block_b. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -307,7 +306,7 @@ def test_safe_target_follows_heavier_fork_on_split( ), # Minority (2/6) attests to block_a. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(4), ValidatorIndex(5), @@ -378,7 +377,7 @@ def test_safe_target_is_conservative_relative_to_lmd_ghost_head( TickStep(time=18), # 6/8 vote for block_2. Weight: block_1 += 6, block_2 += 6. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -394,7 +393,7 @@ def test_safe_target_is_conservative_relative_to_lmd_ghost_head( ), # 2/8 vote for block_3. Weight: block_1 += 2, block_2 += 2, block_3 += 2. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(6), ValidatorIndex(7), @@ -497,7 +496,7 @@ def test_safe_target_ignores_known_pool_at_interval_3( # Gossip 2 more attestations into the "new" pool. # Combined with "known": total weight = 4 = threshold. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(2), ValidatorIndex(3), diff --git a/tests/consensus/lstar/fc/test_store_pruning.py b/tests/consensus/lstar/fc/test_store_pruning.py index 00c021aa3..c9944d245 100644 --- a/tests/consensus/lstar/fc/test_store_pruning.py +++ b/tests/consensus/lstar/fc/test_store_pruning.py @@ -8,7 +8,6 @@ BlockSpec, BlockStep, ForkChoiceTestFiller, - GossipAggregatedAttestationSpec, GossipAggregatedAttestationStep, GossipAttestationSpec, StoreChecks, @@ -138,7 +137,7 @@ def test_finalization_prunes_stale_aggregated_payloads( # # Stale gossip: target=1 (at finalized slot), should be pruned later GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -154,7 +153,7 @@ def test_finalization_prunes_stale_aggregated_payloads( # Fresh gossip: target=5 (above finalized), should survive pruning # V3 is unique to this attestation (not in stale) GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(1), ValidatorIndex(2), @@ -359,7 +358,7 @@ def test_finalization_prunes_stale_attestation_signatures( # correct justified checkpoint for targets that were justified at # their respective slots. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)], slot=Slot(5), target_slot=Slot(1), @@ -369,7 +368,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)], slot=Slot(5), target_slot=Slot(2), @@ -379,7 +378,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)], slot=Slot(5), target_slot=Slot(3), @@ -389,7 +388,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)], slot=Slot(5), target_slot=Slot(4), @@ -399,7 +398,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(0), ValidatorIndex(1), ValidatorIndex(2)], slot=Slot(5), target_slot=Slot(5), @@ -423,7 +422,7 @@ def test_finalization_prunes_stale_attestation_signatures( # Second batch (V3-V5): gossip aggregated attestations for all targets. # These land in latest_new_aggregated_payloads (first batch already migrated). GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(3), ValidatorIndex(4), ValidatorIndex(5)], slot=Slot(5), target_slot=Slot(1), @@ -433,7 +432,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(3), ValidatorIndex(4), ValidatorIndex(5)], slot=Slot(5), target_slot=Slot(2), @@ -443,7 +442,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(3), ValidatorIndex(4), ValidatorIndex(5)], slot=Slot(5), target_slot=Slot(3), @@ -453,7 +452,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(3), ValidatorIndex(4), ValidatorIndex(5)], slot=Slot(5), target_slot=Slot(4), @@ -463,7 +462,7 @@ def test_finalization_prunes_stale_attestation_signatures( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ValidatorIndex(3), ValidatorIndex(4), ValidatorIndex(5)], slot=Slot(5), target_slot=Slot(5), diff --git a/tests/consensus/lstar/fc/test_tick_system.py b/tests/consensus/lstar/fc/test_tick_system.py index c3a87f29b..9c9fb45e7 100644 --- a/tests/consensus/lstar/fc/test_tick_system.py +++ b/tests/consensus/lstar/fc/test_tick_system.py @@ -2,11 +2,11 @@ import pytest from consensus_testing import ( + AggregatedAttestationSpec, AttestationCheck, BlockSpec, BlockStep, ForkChoiceTestFiller, - GossipAggregatedAttestationSpec, GossipAggregatedAttestationStep, StoreChecks, TickStep, @@ -86,7 +86,7 @@ def test_tick_interval_progression_through_full_slot( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -247,7 +247,7 @@ def test_tick_interval_0_skips_acceptance_when_not_proposer( ), # Start with a pending aggregated attestation for slot 3. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -302,7 +302,7 @@ def test_tick_interval_0_skips_acceptance_when_not_proposer( # At store time 19, current slot is still 3, so a slot-4 attestation # is within the allowed +1 future-slot margin. GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(0), ValidatorIndex(1), @@ -355,7 +355,7 @@ def test_tick_interval_0_skips_acceptance_when_not_proposer( ), ), GossipAggregatedAttestationStep( - attestation=GossipAggregatedAttestationSpec( + attestation=AggregatedAttestationSpec( validator_indices=[ ValidatorIndex(1), ValidatorIndex(2), From ab183e397f0702ced737bccad6b7f1170b996de7 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:50:49 +0200 Subject: [PATCH 2/2] docs(testing): tighten attestation spec docstrings Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test_types/attestation_specs.py | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/packages/testing/src/consensus_testing/test_types/attestation_specs.py b/packages/testing/src/consensus_testing/test_types/attestation_specs.py index 76859b01a..f251c976a 100644 --- a/packages/testing/src/consensus_testing/test_types/attestation_specs.py +++ b/packages/testing/src/consensus_testing/test_types/attestation_specs.py @@ -23,12 +23,7 @@ class AttestationSpec(CamelModel): - """ - Shared core for attestation specifications. - - Declares the checkpoint fields (target, head, source) common to all - attestation specs and resolves them into attestation data. - """ + """Shared core for attestation specifications.""" slot: Slot """The slot for which the attestation is made (required).""" @@ -65,15 +60,7 @@ class AttestationSpec(CamelModel): """Optional override for the source checkpoint slot.""" 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 instead of proper XMSS signatures. - - Defaults to True (valid signatures). - """ + """Flag whether the generated attestation signatures should be valid.""" def build_attestation_data( self, @@ -153,14 +140,7 @@ def build_attestation_data( class AggregatedAttestationSpec(AttestationSpec): - """ - Aggregated attestation specification for test definitions. - - Specifies multiple validators attesting to the same data. - Used both for attestations embedded in blocks and for - aggregations received via gossip. - The source defaults to the latest justified checkpoint unless overridden. - """ + """Aggregated attestation specification for test definitions.""" validator_indices: list[ValidatorIndex] """The indices of validators making the attestation (required).""" @@ -169,8 +149,8 @@ class AggregatedAttestationSpec(AttestationSpec): """ Override which validators actually sign the attestation. - When None (default), signatures are generated using the validators in validator_indices. - When specified, signatures are generated using these validator indices instead. + - When None (default), signatures are generated using the validators in validator_indices. + - When specified, signatures are generated using these validator indices instead. This creates a mismatch between claimed participants and actual signers. Useful for testing that verification rejects attestations where valid signatures