From 5a0135ee019565a4fe493f9c6cf1ed5b8117234f Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Wed, 10 Dec 2025 15:37:01 +0530 Subject: [PATCH 01/12] fix: update containers for signature aggregation --- .../containers/attestation/attestation.py | 10 ++++++++-- .../subspecs/containers/block/block.py | 20 +++++++++++++++++-- .../subspecs/containers/block/types.py | 14 ++++++------- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index e4983284..fc3514b9 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -49,7 +49,10 @@ class Attestation(Container): class SignedAttestation(Container): """Validator attestation bundled with its signature.""" - message: Attestation + validator_id: Uint64 + """The index of the validator making the attestation.""" + + message: AttestationData """The attestation message signed by the validator.""" signature: Signature @@ -82,5 +85,8 @@ class SignedAggregatedAttestations(Container): Stores a naive list of validator signatures that mirrors the attestation order. - TODO: this will be replaced by a SNARK in future devnets. + TODO: + - signatures will be replaced by MegaBytes in next PR to include leanVM proof. + - this will be replaced by a SNARK in future devnets. + - this will be aggregated by aggregators in future devnets. """ diff --git a/src/lean_spec/subspecs/containers/block/block.py b/src/lean_spec/subspecs/containers/block/block.py index 5c985932..7eaea3f5 100644 --- a/src/lean_spec/subspecs/containers/block/block.py +++ b/src/lean_spec/subspecs/containers/block/block.py @@ -16,9 +16,10 @@ from lean_spec.types import Bytes32, Uint64 from lean_spec.types.container import Container +from ...xmss.containers import Signature as XmssSignature from ..attestation import Attestation from ..validator import Validator -from .types import Attestations, BlockSignatures +from .types import AggregatedAttestationsList, AttestationSignatures if TYPE_CHECKING: from ..state import State @@ -32,7 +33,7 @@ class BlockBody(Container): packaged into blocks. """ - attestations: Attestations + attestations: AggregatedAttestationsList """Plain validator attestations carried in the block body. Individual signatures live in the aggregated block signature list, so @@ -96,6 +97,21 @@ class BlockWithAttestation(Container): proposer_attestation: Attestation """The proposer's attestation corresponding to this block.""" +class BlockSignatures(Container): + """Signature payload for the block.""" + + attestation_signatures: AttestationSignatures + """Signatures for the attestations in the block body. + + Contains a naive list of signatures for the attestations in the block body. + + TODO: + - this will be replaced by a BytesArray in next PR to include leanVM aggregated sproof. + """ + + proposer_signature: XmssSignature + """Signature for the proposer's attestation.""" + class SignedBlockWithAttestation(Container): """Envelope carrying a block, an attestation from proposer, and aggregated signatures.""" diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index 0e5f68ff..4f910473 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -3,19 +3,19 @@ from lean_spec.types import SSZList from ...chain.config import VALIDATOR_REGISTRY_LIMIT +from ..attestation import AggregatedAttestations, SignedAggregatedAttestations from ...xmss.containers import Signature -from ..attestation import Attestation -class Attestations(SSZList): - """List of validator attestations included in a block.""" +class AggregatedAttestationsList(SSZList): + """List of aggregated attestations included in a block.""" - ELEMENT_TYPE = Attestation + ELEMENT_TYPE = AggregatedAttestations LIMIT = int(VALIDATOR_REGISTRY_LIMIT) - -class BlockSignatures(SSZList): - """Aggregated signature list included alongside the block.""" +class AttestationSignatures(SSZList): + """Aggregated signature list included alongside the block proposer's attestation.""" ELEMENT_TYPE = Signature LIMIT = int(VALIDATOR_REGISTRY_LIMIT) + From 24363edb933fdaa163cd973f00b9665895f96b01 Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Wed, 10 Dec 2025 15:53:07 +0530 Subject: [PATCH 02/12] fix: adjust validator signing, container update changes --- .../testing/src/consensus_testing/keys.py | 26 ++-- .../test_fixtures/fork_choice.py | 37 ++++-- .../test_fixtures/state_transition.py | 23 +++- .../test_types/store_checks.py | 10 +- .../containers/attestation/__init__.py | 6 + .../containers/attestation/attestation.py | 36 ++++++ .../subspecs/containers/block/__init__.py | 8 +- .../subspecs/containers/block/block.py | 69 +++++------ .../subspecs/containers/block/types.py | 4 +- .../subspecs/containers/state/state.py | 38 ++++-- src/lean_spec/subspecs/forkchoice/store.py | 46 ++++--- .../devnet/state_transition/test_genesis.py | 16 +-- .../lean_spec/subspecs/forkchoice/conftest.py | 12 +- .../forkchoice/test_time_management.py | 10 +- .../subspecs/forkchoice/test_validator.py | 13 +- tests/lean_spec/subspecs/ssz/test_block.py | 112 ++++++++++-------- .../subspecs/ssz/test_signed_attestation.py | 85 ++++++------- 17 files changed, 326 insertions(+), 225 deletions(-) diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index ce2f6bdb..f8d15a3e 100644 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -29,7 +29,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Iterator, Self -from lean_spec.subspecs.containers import Attestation +from lean_spec.subspecs.containers import AttestationData from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.containers import PublicKey, SecretKey, Signature @@ -121,7 +121,7 @@ class XmssKeyManager: >>> mgr = XmssKeyManager() >>> mgr[Uint64(0)] # Get key pair >>> mgr.get_public_key(Uint64(1)) # Get public key only - >>> mgr.sign_attestation(attestation) # Sign with auto-advancement + >>> mgr.sign_attestation_data(validator_id, attestation_data) # Sign with auto-advancement """ def __init__( @@ -167,15 +167,20 @@ def get_all_public_keys(self) -> dict[Uint64, PublicKey]: """Get all public keys (from base keys, not advanced state).""" return {idx: kp.public for idx, kp in self.keys.items()} - def sign_attestation(self, attestation: Attestation) -> Signature: + def sign_attestation_data( + self, + validator_id: Uint64, + attestation_data: AttestationData, + ) -> Signature: """ - Sign an attestation with automatic key state advancement. + Sign an attestation data with automatic key state advancement. XMSS is stateful: signing advances the internal key state. This method handles advancement transparently. Args: - attestation: The attestation to sign. + validator_id: The validator index to sign the attestation data for. + attestation_data: The attestation data to sign. Returns: XMSS signature. @@ -183,9 +188,8 @@ def sign_attestation(self, attestation: Attestation) -> Signature: Raises: ValueError: If slot exceeds key lifetime. """ - idx = attestation.validator_id - epoch = attestation.data.slot - kp = self[idx] + epoch = attestation_data.slot + kp = self[validator_id] sk = kp.secret # Advance key state until epoch is in prepared interval @@ -198,10 +202,10 @@ def sign_attestation(self, attestation: Attestation) -> Signature: prepared = self.scheme.get_prepared_interval(sk) # Cache advanced state - self._state[idx] = kp.with_secret(sk) + self._state[validator_id] = kp.with_secret(sk) - # Sign hash tree root - message = bytes(hash_tree_root(attestation)) + # Sign hash tree root of the attestation data + message = bytes(hash_tree_root(attestation_data)) return self.scheme.sign(sk, epoch, message) 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 b4d3d133..f81cacd7 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py +++ b/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py @@ -13,13 +13,17 @@ AttestationData, SignedAttestation, ) -from lean_spec.subspecs.containers.block.block import ( +from lean_spec.subspecs.containers.block import ( Block, BlockBody, + BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation, ) -from lean_spec.subspecs.containers.block.types import Attestations, BlockSignatures +from lean_spec.subspecs.containers.block.types import ( + AggregatedAttestationsList, + AttestationSignatures, +) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators @@ -133,7 +137,7 @@ def set_anchor_block_default(self) -> ForkChoiceTest: proposer_index=self.anchor_state.latest_block_header.proposer_index, parent_root=self.anchor_state.latest_block_header.parent_root, state_root=hash_tree_root(self.anchor_state), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) return self @@ -153,7 +157,7 @@ def set_max_slot_default(self) -> ForkChoiceTest: if isinstance(step, BlockStep): max_slot_value = max(max_slot_value, int(step.block.slot)) elif isinstance(step, AttestationStep): - max_slot_value = max(max_slot_value, int(step.attestation.message.data.slot)) + max_slot_value = max(max_slot_value, int(step.attestation.message.slot)) self.max_slot = Slot(max_slot_value) @@ -344,15 +348,23 @@ def _build_block_from_spec( ) # Sign all attestations and the proposer attestation - signature_list = [key_manager.sign_attestation(att) for att in attestations] - signature_list.append(key_manager.sign_attestation(proposer_attestation)) + attestation_signatures = [ + key_manager.sign_attestation_data(att.validator_id, att.data) for att in attestations + ] + proposer_signature = key_manager.sign_attestation_data( + proposer_attestation.validator_id, + proposer_attestation.data, + ) return SignedBlockWithAttestation( message=BlockWithAttestation( block=final_block, proposer_attestation=proposer_attestation, ), - signature=BlockSignatures(data=signature_list), + signature=BlockSignatures( + attestation_signatures=AttestationSignatures(data=attestation_signatures), + proposer_signature=proposer_signature, + ), ) def _resolve_parent_root( @@ -415,9 +427,13 @@ def _build_attestations_from_spec( signed_att = self._build_signed_attestation_from_spec( att_spec, block_registry, parent_state ) - attestations.append(signed_att.message) + attestations.append( + Attestation(validator_id=signed_att.validator_id, data=signed_att.message) + ) else: - attestations.append(att_spec.message) + attestations.append( + Attestation(validator_id=att_spec.validator_id, data=att_spec.message) + ) return attestations @@ -473,7 +489,8 @@ def _build_signed_attestation_from_spec( # Create signed attestation return SignedAttestation( - message=attestation, + validator_id=attestation.validator_id, + message=attestation.data, signature=( spec.signature or Signature( 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 5e6ca0ed..59441059 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -4,8 +4,12 @@ from pydantic import ConfigDict, PrivateAttr, field_serializer +from lean_spec.subspecs.containers.attestation import ( + aggregated_attestation_to_plain, + attestation_to_aggregated, +) from lean_spec.subspecs.containers.block.block import Block, BlockBody -from lean_spec.subspecs.containers.block.types import Attestations +from lean_spec.subspecs.containers.block.types import AggregatedAttestationsList from lean_spec.subspecs.containers.state.state import State from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Uint64 @@ -204,8 +208,12 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, temp_state = state.process_slots(spec.slot) parent_root = hash_tree_root(temp_state.latest_block_header) - # Extract attestations from body if provided - attestations = list(spec.body.attestations) if spec.body else [] + # Extract attestations from body if provided, converting from aggregated form + attestations = ( + [aggregated_attestation_to_plain(att) for att in spec.body.attestations] + if spec.body + else [] + ) # Handle explicit state root override if spec.state_root is not None: @@ -214,7 +222,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, proposer_index=proposer_index, parent_root=parent_root, state_root=spec.state_root, - body=spec.body or BlockBody(attestations=Attestations(data=[])), + body=spec.body or BlockBody(attestations=AggregatedAttestationsList(data=[])), ) return block, None @@ -225,7 +233,12 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, proposer_index=proposer_index, parent_root=parent_root, state_root=Bytes32.zero(), - body=spec.body or BlockBody(attestations=Attestations(data=attestations)), + body=spec.body + or BlockBody( + attestations=AggregatedAttestationsList( + data=[attestation_to_aggregated(att) for att in attestations] + ) + ), ) return block, None diff --git a/packages/testing/src/consensus_testing/test_types/store_checks.py b/packages/testing/src/consensus_testing/test_types/store_checks.py index d248dace..0fbef3d5 100644 --- a/packages/testing/src/consensus_testing/test_types/store_checks.py +++ b/packages/testing/src/consensus_testing/test_types/store_checks.py @@ -51,7 +51,7 @@ def validate_attestation( expected = getattr(self, field_name) if field_name == "attestation_slot": - actual = attestation.message.data.slot + actual = attestation.message.slot if actual != expected: raise AssertionError( f"Step {step_index}: validator {self.validator} {location} " @@ -59,7 +59,7 @@ def validate_attestation( ) elif field_name == "head_slot": - actual = attestation.message.data.head.slot + actual = attestation.message.head.slot if actual != expected: raise AssertionError( f"Step {step_index}: validator {self.validator} {location} " @@ -67,7 +67,7 @@ def validate_attestation( ) elif field_name == "source_slot": - actual = attestation.message.data.source.slot + actual = attestation.message.source.slot if actual != expected: raise AssertionError( f"Step {step_index}: validator {self.validator} {location} " @@ -75,7 +75,7 @@ def validate_attestation( ) elif field_name == "target_slot": - actual = attestation.message.data.target.slot + actual = attestation.message.target.slot if actual != expected: raise AssertionError( f"Step {step_index}: validator {self.validator} {location} " @@ -442,7 +442,7 @@ def validate_against_store( # An attestation votes for this fork if its head is this block or a descendant weight = 0 for attestation in store.latest_known_attestations.values(): - att_head_root = attestation.message.data.head.root + att_head_root = attestation.message.head.root # Check if attestation head is this block or a descendant if att_head_root == root: weight += 1 diff --git a/src/lean_spec/subspecs/containers/attestation/__init__.py b/src/lean_spec/subspecs/containers/attestation/__init__.py index 08526865..b0e6bc2d 100644 --- a/src/lean_spec/subspecs/containers/attestation/__init__.py +++ b/src/lean_spec/subspecs/containers/attestation/__init__.py @@ -6,6 +6,9 @@ AttestationData, SignedAggregatedAttestations, SignedAttestation, + aggregated_attestation_to_plain, + aggregation_bits_to_validator_index, + attestation_to_aggregated, ) from .types import AggregatedSignatures, AggregationBits @@ -17,4 +20,7 @@ "AggregatedAttestations", "AggregatedSignatures", "AggregationBits", + "aggregation_bits_to_validator_index", + "aggregated_attestation_to_plain", + "attestation_to_aggregated", ] diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index fc3514b9..406784e2 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -90,3 +90,39 @@ class SignedAggregatedAttestations(Container): - this will be replaced by a SNARK in future devnets. - this will be aggregated by aggregators in future devnets. """ + + +def _aggregation_bits_to_validator_index(bits: AggregationBits) -> Uint64: + """ + Extract the single validator index encoded in aggregation bits. + + Current devnets only support naive aggregation where every attestation + includes exactly one participant. The bitlist therefore acts as a compact + encoding of the validator index. + """ + participants = [index for index, bit in enumerate(bits) if bool(bit)] + if len(participants) != 1: + raise AssertionError("Aggregated attestation must reference exactly one validator") + return Uint64(participants[0]) + + +def aggregation_bits_to_validator_index(bits: AggregationBits) -> Uint64: + """Public helper wrapper for extracting the validator index from bits.""" + return _aggregation_bits_to_validator_index(bits) + + +def aggregated_attestation_to_plain(aggregated: AggregatedAttestations) -> Attestation: + """Convert aggregated attestation data to the plain Attestation container.""" + validator_index = _aggregation_bits_to_validator_index(aggregated.aggregation_bits) + return Attestation(validator_id=validator_index, data=aggregated.data) + + +def attestation_to_aggregated(attestation: Attestation) -> AggregatedAttestations: + """Convert a plain Attestation into the aggregated representation.""" + validator_index = int(attestation.validator_id) + bits = [False] * (validator_index + 1) + bits[validator_index] = True + return AggregatedAttestations( + aggregation_bits=AggregationBits(data=bits), + data=attestation.data, + ) diff --git a/src/lean_spec/subspecs/containers/block/__init__.py b/src/lean_spec/subspecs/containers/block/__init__.py index a3a844cd..d036e142 100644 --- a/src/lean_spec/subspecs/containers/block/__init__.py +++ b/src/lean_spec/subspecs/containers/block/__init__.py @@ -4,17 +4,19 @@ Block, BlockBody, BlockHeader, + BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation, ) -from .types import Attestations, BlockSignatures +from .types import AggregatedAttestationsList, AttestationSignatures __all__ = [ "Block", "BlockBody", "BlockHeader", + "BlockSignatures", "BlockWithAttestation", "SignedBlockWithAttestation", - "Attestations", - "BlockSignatures", + "AggregatedAttestationsList", + "AttestationSignatures", ] diff --git a/src/lean_spec/subspecs/containers/block/block.py b/src/lean_spec/subspecs/containers/block/block.py index 7eaea3f5..17cc4144 100644 --- a/src/lean_spec/subspecs/containers/block/block.py +++ b/src/lean_spec/subspecs/containers/block/block.py @@ -17,7 +17,7 @@ from lean_spec.types.container import Container from ...xmss.containers import Signature as XmssSignature -from ..attestation import Attestation +from ..attestation import Attestation, aggregation_bits_to_validator_index from ..validator import Validator from .types import AggregatedAttestationsList, AttestationSignatures @@ -97,12 +97,13 @@ class BlockWithAttestation(Container): proposer_attestation: Attestation """The proposer's attestation corresponding to this block.""" + class BlockSignatures(Container): """Signature payload for the block.""" attestation_signatures: AttestationSignatures """Signatures for the attestations in the block body. - + Contains a naive list of signatures for the attestations in the block body. TODO: @@ -153,48 +154,48 @@ def verify_signatures(self, parent_state: "State") -> bool: - Validator index out of range - XMSS signature verification failure """ - # Unpack the signed block components block = self.message.block signatures = self.signature + block_attestations = block.body.attestations + attestation_signatures = signatures.attestation_signatures - # Combine all attestations that need verification - # - # This creates a single list containing both: - # 1. Block body attestations (from other validators) - # 2. Proposer attestation (from the block producer) - all_attestations = block.body.attestations + [self.message.proposer_attestation] - - # Verify signature count matches attestation count - # - # Each attestation must have exactly one corresponding signature. - # - # The ordering must be preserved: - # 1. Block body attestations, - # 2. The proposer attestation. - assert len(signatures) == len(all_attestations), ( - "Number of signatures does not match number of attestations" + # Verify signature count matches attestation count. + assert len(attestation_signatures) == len(block_attestations), ( + "Number of attestation signatures does not match attestation count" ) validators = parent_state.validators - # Verify each attestation signature - for attestation, signature in zip(all_attestations, signatures, strict=True): - # Ensure validator exists in the active set - assert attestation.validator_id < Uint64(len(validators)), ( - "Validator index out of range" + # Verify each block body attestation signature + for aggregated_attestation, signature in zip( + block_attestations, attestation_signatures, strict=True + ): + validator_id = aggregation_bits_to_validator_index( + aggregated_attestation.aggregation_bits ) - validator = cast(Validator, validators[attestation.validator_id]) - - # Verify the XMSS signature - # - # This cryptographically proves that: - # - The validator possesses the secret key for their public key - # - The attestation has not been tampered with - # - The signature was created at the correct epoch (slot) + + # Ensure validator exists in the active set + assert validator_id < Uint64(len(validators)), "Validator index out of range" + validator = cast(Validator, validators[validator_id]) + assert signature.verify( validator.get_pubkey(), - attestation.data.slot, - bytes(hash_tree_root(attestation)), + aggregated_attestation.data.slot, + bytes(hash_tree_root(aggregated_attestation.data)), ), "Attestation signature verification failed" + # Verify proposer attestation signature + proposer_attestation = self.message.proposer_attestation + proposer_signature = signatures.proposer_signature + assert proposer_attestation.validator_id < Uint64(len(validators)), ( + "Proposer index out of range" + ) + proposer = cast(Validator, validators[proposer_attestation.validator_id]) + + assert proposer_signature.verify( + proposer.get_pubkey(), + proposer_attestation.data.slot, + bytes(hash_tree_root(proposer_attestation.data)), + ), "Proposer signature verification failed" + return True diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index 4f910473..08a472ce 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -3,8 +3,8 @@ from lean_spec.types import SSZList from ...chain.config import VALIDATOR_REGISTRY_LIMIT -from ..attestation import AggregatedAttestations, SignedAggregatedAttestations from ...xmss.containers import Signature +from ..attestation import AggregatedAttestations class AggregatedAttestationsList(SSZList): @@ -13,9 +13,9 @@ class AggregatedAttestationsList(SSZList): ELEMENT_TYPE = AggregatedAttestations LIMIT = int(VALIDATOR_REGISTRY_LIMIT) + class AttestationSignatures(SSZList): """Aggregated signature list included alongside the block proposer's attestation.""" ELEMENT_TYPE = Signature LIMIT = int(VALIDATOR_REGISTRY_LIMIT) - diff --git a/src/lean_spec/subspecs/containers/state/state.py b/src/lean_spec/subspecs/containers/state/state.py index 9617f9f6..07e9f8cf 100644 --- a/src/lean_spec/subspecs/containers/state/state.py +++ b/src/lean_spec/subspecs/containers/state/state.py @@ -12,12 +12,17 @@ is_proposer, ) -from ..attestation import Attestation, SignedAttestation +from ..attestation import ( + Attestation, + SignedAttestation, + aggregated_attestation_to_plain, + attestation_to_aggregated, +) if TYPE_CHECKING: from lean_spec.subspecs.xmss.containers import Signature from ..block import Block, BlockBody, BlockHeader -from ..block.types import Attestations +from ..block.types import AggregatedAttestationsList from ..checkpoint import Checkpoint from ..config import Config from ..slot import Slot @@ -96,7 +101,7 @@ def generate_genesis(cls, genesis_time: Uint64, validators: Validators) -> "Stat proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body_root=hash_tree_root(BlockBody(attestations=Attestations(data=[]))), + body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationsList(data=[]))), ) # Assemble and return the full genesis state. @@ -356,12 +361,13 @@ def process_block(self, block: Block) -> "State": # First process the block header. state = self.process_block_header(block) - # Process justification attestations. - return state.process_attestations(block.body.attestations) + # Process justification attestations by converting aggregated payloads + attestations = [aggregated_attestation_to_plain(att) for att in block.body.attestations] + return state.process_attestations(attestations) def process_attestations( self, - attestations: Attestations, + attestations: Iterable[Attestation], ) -> "State": """ Apply attestations and update justification/finalization @@ -374,8 +380,8 @@ def process_attestations( Parameters ---------- - attestations : Attestations - The list of attestations to process. + attestations : Iterable[Attestation] + The attestations to process. Returns: ------- @@ -649,7 +655,11 @@ def build_block( proposer_index=proposer_index, parent_root=parent_root, state_root=Bytes32.zero(), - body=BlockBody(attestations=Attestations(data=attestations)), + body=BlockBody( + attestations=AggregatedAttestationsList( + data=[attestation_to_aggregated(att) for att in attestations] + ) + ), ) # Apply state transition to get the post-block state @@ -664,7 +674,11 @@ def build_block( new_signatures: list[Signature] = [] for signed_attestation in available_signed_attestations: - data = signed_attestation.message.data + data = signed_attestation.message + attestation = Attestation( + validator_id=signed_attestation.validator_id, + data=data, + ) # Skip if target block is unknown if data.head.root not in known_block_roots: @@ -675,8 +689,8 @@ def build_block( continue # Add attestation if not already included - if signed_attestation.message not in attestations: - new_attestations.append(signed_attestation.message) + if attestation not in attestations: + new_attestations.append(attestation) new_signatures.append(signed_attestation.signature) # Fixed point reached: no new attestations found diff --git a/src/lean_spec/subspecs/forkchoice/store.py b/src/lean_spec/subspecs/forkchoice/store.py index 2f48ff13..872b6f12 100644 --- a/src/lean_spec/subspecs/forkchoice/store.py +++ b/src/lean_spec/subspecs/forkchoice/store.py @@ -30,6 +30,7 @@ SignedBlockWithAttestation, State, ) +from lean_spec.subspecs.containers.attestation import aggregation_bits_to_validator_index from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.containers import Signature @@ -211,7 +212,7 @@ def validate_attestation(self, signed_attestation: SignedAttestation) -> None: Raises: AssertionError: If attestation fails validation. """ - data = signed_attestation.message.data + data = signed_attestation.message # Availability Check # @@ -293,11 +294,11 @@ def on_attestation( self.validate_attestation(signed_attestation) # Extract the validator index that produced this attestation. - validator_id = Uint64(signed_attestation.message.validator_id) + validator_id = Uint64(signed_attestation.validator_id) # Extract the attestation's slot: # - used to decide if this attestation is "newer" than a previous one. - attestation_slot = signed_attestation.message.data.slot + attestation_slot = signed_attestation.message.slot # Copy the known attestation map: # - we build a new Store immutably, @@ -321,7 +322,7 @@ def on_attestation( # Update the known attestation for this validator if: # - there is no known attestation yet, or # - this attestation is from a later slot than the known one. - if latest_known is None or latest_known.message.data.slot < attestation_slot: + if latest_known is None or latest_known.message.slot < attestation_slot: new_known[validator_id] = signed_attestation # Fetch any pending ("new") attestation for this validator. @@ -332,7 +333,7 @@ def on_attestation( # - it is from an equal or earlier slot than this on-chain attestation. # # In that case, the on-chain attestation supersedes it. - if existing_new is not None and existing_new.message.data.slot <= attestation_slot: + if existing_new is not None and existing_new.message.slot <= attestation_slot: del new_new[validator_id] else: # Network gossip attestation processing @@ -355,7 +356,7 @@ def on_attestation( # Update the pending attestation for this validator if: # - there is no pending attestation yet, or # - this one is from a later slot than the pending one. - if latest_new is None or latest_new.message.data.slot < attestation_slot: + if latest_new is None or latest_new.message.slot < attestation_slot: new_new[validator_id] = signed_attestation # Return a new Store with updated "known" and "new" attestation maps. @@ -410,7 +411,6 @@ def on_block(self, signed_block_with_attestation: SignedBlockWithAttestation) -> # Unpack block components block = signed_block_with_attestation.message.block proposer_attestation = signed_block_with_attestation.message.proposer_attestation - signatures = signed_block_with_attestation.signature block_root = hash_tree_root(block) # Skip duplicate blocks (idempotent operation) @@ -457,18 +457,25 @@ def on_block(self, signed_block_with_attestation: SignedBlockWithAttestation) -> } ) - # Process block body attestations - # - # Iterate over attestations and their corresponding signatures. - for attestation, signature in zip( - signed_block_with_attestation.message.block.body.attestations, - signed_block_with_attestation.signature, - strict=False, + # Process block body attestations. + block_attestations = signed_block_with_attestation.message.block.body.attestations + attestation_signatures = signed_block_with_attestation.signature.attestation_signatures + + assert len(block_attestations) == len(attestation_signatures), ( + "Attestation signature list must align with block attestations" + ) + + for aggregated_attestation, signature in zip( + block_attestations, attestation_signatures, strict=True ): - # Process as on-chain attestation (immediately becomes "known") + validator_id = aggregation_bits_to_validator_index( + aggregated_attestation.aggregation_bits + ) + store = store.on_attestation( signed_attestation=SignedAttestation( - message=attestation, + validator_id=validator_id, + message=aggregated_attestation.data, signature=signature, ), is_from_block=True, @@ -489,8 +496,9 @@ def on_block(self, signed_block_with_attestation: SignedBlockWithAttestation) -> # 3. Influence fork choice only after interval 3 (end of slot) store = store.on_attestation( signed_attestation=SignedAttestation( - message=proposer_attestation, - signature=signatures[len(block.body.attestations)], + validator_id=proposer_attestation.validator_id, + message=proposer_attestation.data, + signature=signed_block_with_attestation.signature.proposer_signature, ), is_from_block=False, ) @@ -552,7 +560,7 @@ def _compute_lmd_ghost_head( # # Each visited block accumulates one unit of weight from that validator. for attestation in attestations.values(): - current_root = attestation.message.data.head.root + current_root = attestation.message.head.root # Climb towards the anchor while staying inside the known tree. # diff --git a/tests/consensus/devnet/state_transition/test_genesis.py b/tests/consensus/devnet/state_transition/test_genesis.py index b991cc30..2f961b86 100644 --- a/tests/consensus/devnet/state_transition/test_genesis.py +++ b/tests/consensus/devnet/state_transition/test_genesis.py @@ -11,7 +11,7 @@ from consensus_testing import StateExpectation, StateTransitionTestFiller, generate_pre_state from lean_spec.subspecs.containers.block import Block, BlockBody -from lean_spec.subspecs.containers.block.types import Attestations +from lean_spec.subspecs.containers.block.types import AggregatedAttestationsList from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import State, Validators from lean_spec.subspecs.containers.state.types import ( @@ -55,7 +55,7 @@ def test_genesis_default_configuration( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=Attestations(data=[])) + BlockBody(attestations=AggregatedAttestationsList(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -100,7 +100,7 @@ def test_genesis_custom_time( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=Attestations(data=[])) + BlockBody(attestations=AggregatedAttestationsList(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -143,7 +143,7 @@ def test_genesis_custom_validator_set( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=Attestations(data=[])) + BlockBody(attestations=AggregatedAttestationsList(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -173,7 +173,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state1), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) # Compute hash of first genesis block @@ -190,7 +190,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state1_copy), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) genesis_block_hash1_copy = hash_tree_root(genesis_block1_copy) @@ -215,7 +215,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state2), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) genesis_block_hash2 = hash_tree_root(genesis_block2) @@ -240,7 +240,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state3), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) genesis_block_hash3 = hash_tree_root(genesis_block3) diff --git a/tests/lean_spec/subspecs/forkchoice/conftest.py b/tests/lean_spec/subspecs/forkchoice/conftest.py index d8b97a0e..454edd98 100644 --- a/tests/lean_spec/subspecs/forkchoice/conftest.py +++ b/tests/lean_spec/subspecs/forkchoice/conftest.py @@ -5,14 +5,13 @@ import pytest from lean_spec.subspecs.containers import ( - Attestation, AttestationData, BlockBody, Checkpoint, SignedAttestation, State, ) -from lean_spec.subspecs.containers.block import Attestations, BlockHeader +from lean_spec.subspecs.containers.block import AggregatedAttestationsList, BlockHeader from lean_spec.subspecs.containers.config import Config from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators @@ -45,7 +44,7 @@ def __init__(self, latest_justified: Checkpoint) -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body_root=hash_tree_root(BlockBody(attestations=Attestations(data=[]))), + body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationsList(data=[]))), ) super().__init__( @@ -76,12 +75,9 @@ def build_signed_attestation( target=target, source=source_checkpoint, ) - message = Attestation( - validator_id=validator, - data=attestation_data, - ) return SignedAttestation( - message=message, + validator_id=validator, + message=attestation_data, signature=Signature( path=HashTreeOpening(siblings=HashDigestList(data=[])), rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), diff --git a/tests/lean_spec/subspecs/forkchoice/test_time_management.py b/tests/lean_spec/subspecs/forkchoice/test_time_management.py index c4e9260b..66a8a49e 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_time_management.py +++ b/tests/lean_spec/subspecs/forkchoice/test_time_management.py @@ -10,7 +10,7 @@ State, Validator, ) -from lean_spec.subspecs.containers.block import Attestations +from lean_spec.subspecs.containers.block import AggregatedAttestationsList from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators from lean_spec.subspecs.forkchoice import Store @@ -35,7 +35,7 @@ def sample_store(sample_config: Config) -> Store: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32(b"state" + b"\x00" * 27), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) genesis_hash = hash_tree_root(genesis_block) @@ -281,7 +281,7 @@ def test_accept_new_attestations_multiple(self, sample_store: Store) -> None: # Verify correct mapping for i, checkpoint in enumerate(checkpoints): stored = sample_store.latest_known_attestations[Uint64(i)] - assert stored.message.data.target == checkpoint + assert stored.message.target == checkpoint def test_accept_new_attestations_empty(self, sample_store: Store) -> None: """Test accepting new attestations when there are none.""" @@ -306,7 +306,7 @@ def test_get_proposal_head_basic(self, sample_store: Store) -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32(b"genesis" + b"\x00" * 25), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) genesis_hash = hash_tree_root(genesis_block) @@ -353,7 +353,7 @@ def test_get_proposal_head_processes_attestations(self, sample_store: Store) -> assert Uint64(10) not in store.latest_new_attestations assert Uint64(10) in store.latest_known_attestations stored = store.latest_known_attestations[Uint64(10)] - assert stored.message.data.target == checkpoint + assert stored.message.target == checkpoint class TestTimeConstants: diff --git a/tests/lean_spec/subspecs/forkchoice/test_validator.py b/tests/lean_spec/subspecs/forkchoice/test_validator.py index 54c0b353..3470895f 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_validator.py +++ b/tests/lean_spec/subspecs/forkchoice/test_validator.py @@ -14,7 +14,7 @@ State, Validator, ) -from lean_spec.subspecs.containers.block import Attestations +from lean_spec.subspecs.containers.block import AggregatedAttestationsList from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import ( HistoricalBlockHashes, @@ -82,7 +82,7 @@ def sample_store(config: Config, sample_state: State) -> Store: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(sample_state), - body=BlockBody(attestations=Attestations(data=[])), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), ) genesis_hash = hash_tree_root(genesis_block) @@ -134,12 +134,9 @@ def build_signed_attestation( target=target, source=source, ) - message = Attestation( - validator_id=validator, - data=data, - ) return SignedAttestation( - message=message, + validator_id=validator, + message=data, signature=Signature( path=HashTreeOpening(siblings=HashDigestList(data=[])), rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), @@ -518,7 +515,7 @@ def test_validator_operations_empty_store(self) -> None: config = Config(genesis_time=Uint64(1000)) # Create minimal genesis block first - genesis_body = BlockBody(attestations=Attestations(data=[])) + genesis_body = BlockBody(attestations=AggregatedAttestationsList(data=[])) # Create validators list with 3 validators validators = Validators( diff --git a/tests/lean_spec/subspecs/ssz/test_block.py b/tests/lean_spec/subspecs/ssz/test_block.py index 685d78bd..bf38a0d0 100644 --- a/tests/lean_spec/subspecs/ssz/test_block.py +++ b/tests/lean_spec/subspecs/ssz/test_block.py @@ -1,49 +1,63 @@ -from lean_spec.subspecs.containers.attestation import ( - Attestation, - AttestationData, -) -from lean_spec.subspecs.containers.block.block import ( - Block, - BlockBody, - BlockWithAttestation, - SignedBlockWithAttestation, -) -from lean_spec.subspecs.containers.block.types import Attestations, BlockSignatures -from lean_spec.subspecs.containers.checkpoint import Checkpoint -from lean_spec.types import Bytes32, Uint64 - - -def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: - signed_block_with_attestation = SignedBlockWithAttestation( - message=BlockWithAttestation( - block=Block( - slot=0, - proposer_index=Uint64(0), - parent_root=Bytes32.zero(), - state_root=Bytes32.zero(), - body=BlockBody(attestations=Attestations(data=[])), - ), - proposer_attestation=Attestation( - validator_id=Uint64(0), - data=AttestationData( - slot=0, - head=Checkpoint(root=Bytes32.zero(), slot=0), - target=Checkpoint(root=Bytes32.zero(), slot=0), - source=Checkpoint(root=Bytes32.zero(), slot=0), - ), - ), - ), - signature=BlockSignatures(data=[]), - ) - - encode = signed_block_with_attestation.encode_bytes() - expected_value = ( - "08000000ec0000008c000000000000000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - "0000005400000004000000" - ) - assert encode.hex() == expected_value - assert SignedBlockWithAttestation.decode_bytes(encode) == signed_block_with_attestation +from lean_spec.subspecs.containers.attestation import Attestation, AttestationData +from lean_spec.subspecs.containers.block import ( + Block, + BlockBody, + BlockSignatures, + BlockWithAttestation, + SignedBlockWithAttestation, +) +from lean_spec.subspecs.containers.block.types import ( + AggregatedAttestationsList, + AttestationSignatures, +) +from lean_spec.subspecs.containers.checkpoint import Checkpoint +from lean_spec.subspecs.koalabear import Fp +from lean_spec.subspecs.xmss.constants import PROD_CONFIG +from lean_spec.subspecs.xmss.containers import Signature +from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness +from lean_spec.types import Bytes32, Uint64 + + +def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: + signed_block_with_attestation = SignedBlockWithAttestation( + message=BlockWithAttestation( + block=Block( + slot=0, + proposer_index=Uint64(0), + parent_root=Bytes32.zero(), + state_root=Bytes32.zero(), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + ), + proposer_attestation=Attestation( + validator_id=Uint64(0), + data=AttestationData( + slot=0, + head=Checkpoint(root=Bytes32.zero(), slot=0), + target=Checkpoint(root=Bytes32.zero(), slot=0), + source=Checkpoint(root=Bytes32.zero(), slot=0), + ), + ), + ), + signature=BlockSignatures( + attestation_signatures=AttestationSignatures(data=[]), + proposer_signature=Signature( + path=HashTreeOpening(siblings=HashDigestList(data=[])), + rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), + hashes=HashDigestList(data=[]), + ), + ), + ) + + encode = signed_block_with_attestation.encode_bytes() + expected_value = ( + "08000000ec0000008c00000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000000000000000000000000" + "00000000000000000000000000000054000000040000000800000008000000240000000" + "00000000000000000000000000000000000000000000000000000002800000004000000" + ) + assert encode.hex() == expected_value, "Encoded value must match hardcoded expected value" + assert SignedBlockWithAttestation.decode_bytes(encode) == signed_block_with_attestation diff --git a/tests/lean_spec/subspecs/ssz/test_signed_attestation.py b/tests/lean_spec/subspecs/ssz/test_signed_attestation.py index a7a416fa..f30e6fc0 100644 --- a/tests/lean_spec/subspecs/ssz/test_signed_attestation.py +++ b/tests/lean_spec/subspecs/ssz/test_signed_attestation.py @@ -1,46 +1,39 @@ -from lean_spec.subspecs.containers import ( - Attestation, - AttestationData, - Checkpoint, - SignedAttestation, -) -from lean_spec.subspecs.koalabear import Fp -from lean_spec.subspecs.xmss.constants import PROD_CONFIG -from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness -from lean_spec.types import Bytes32, Uint64 - - -def test_encode_decode_signed_attestation_roundtrip() -> None: - signed_attestation = SignedAttestation( - message=Attestation( - validator_id=Uint64(0), - data=AttestationData( - slot=0, - head=Checkpoint(root=Bytes32.zero(), slot=0), - target=Checkpoint(root=Bytes32.zero(), slot=0), - source=Checkpoint(root=Bytes32.zero(), slot=0), - ), - ), - signature=Signature( - path=HashTreeOpening(siblings=HashDigestList(data=[])), - rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), - hashes=HashDigestList(data=[]), - ), - ) - - # Test that encoding produces the expected hardcoded value - encode = signed_attestation.encode_bytes() - expected_value = ( - "000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000000000000000000000000000000000" - "000000000000000000000000000000008c00000024000000000000000000" - "000000000000000000000000000000000000000000002800000004000000" - ) - assert encode.hex() == expected_value, "Encoded value must match hardcoded expected value" - - # Test that decoding round-trips correctly - decoded = SignedAttestation.decode_bytes(encode) - assert decoded == signed_attestation +from lean_spec.subspecs.containers import AttestationData, Checkpoint, SignedAttestation +from lean_spec.subspecs.koalabear import Fp +from lean_spec.subspecs.xmss.constants import PROD_CONFIG +from lean_spec.subspecs.xmss.containers import Signature +from lean_spec.subspecs.xmss.types import HashDigestList, HashTreeOpening, Randomness +from lean_spec.types import Bytes32, Uint64 + + +def test_encode_decode_signed_attestation_roundtrip() -> None: + attestation_data = AttestationData( + slot=0, + head=Checkpoint(root=Bytes32.zero(), slot=0), + target=Checkpoint(root=Bytes32.zero(), slot=0), + source=Checkpoint(root=Bytes32.zero(), slot=0), + ) + signed_attestation = SignedAttestation( + validator_id=Uint64(0), + message=attestation_data, + signature=Signature( + path=HashTreeOpening(siblings=HashDigestList(data=[])), + rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), + hashes=HashDigestList(data=[]), + ), + ) + + # Test that encoding produces the expected hardcoded value + encoded = signed_attestation.encode_bytes() + expected_value = ( + "000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000000000000000000000000000000008c00000024000000" + "000000000000000000000000000000000000000000000000000000002800000004000000" + ) + + assert encoded.hex() == expected_value, "Encoded value must match hardcoded expected value" + # Test that decoding round-trips correctly + decoded = SignedAttestation.decode_bytes(encoded) + assert decoded == signed_attestation From 7b0b3a62216bbe8a1109b3a38cc902e42270669e Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Wed, 10 Dec 2025 18:02:53 +0530 Subject: [PATCH 03/12] fix: failing tests --- .../test_fixtures/verify_signatures.py | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py index f9c53c3f..348c2500 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -12,11 +12,12 @@ AttestationData, SignedAttestation, ) -from lean_spec.subspecs.containers.block.block import ( +from lean_spec.subspecs.containers.block import ( + BlockSignatures, BlockWithAttestation, SignedBlockWithAttestation, ) -from lean_spec.subspecs.containers.block.types import BlockSignatures +from lean_spec.subspecs.containers.block.types import AttestationSignatures from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state.state import State @@ -216,7 +217,10 @@ def _build_block_from_spec( # Sign proposer attestation - use valid or dummy signature based on spec if spec.valid_signature: - proposer_attestation_signature = key_manager.sign_attestation(proposer_attestation) + proposer_attestation_signature = key_manager.sign_attestation_data( + proposer_attestation.validator_id, + proposer_attestation.data, + ) else: # Generate an invalid dummy signature (all zeros) from lean_spec.subspecs.xmss.constants import TEST_CONFIG @@ -229,14 +233,15 @@ def _build_block_from_spec( hashes=HashDigestList(data=[]), ) - signatures.append(proposer_attestation_signature) - return SignedBlockWithAttestation( message=BlockWithAttestation( block=final_block, proposer_attestation=proposer_attestation, ), - signature=BlockSignatures(data=signatures), + signature=BlockSignatures( + attestation_signatures=AttestationSignatures(data=signatures), + proposer_signature=proposer_attestation_signature, + ), ) def _build_attestations_from_spec( @@ -257,10 +262,22 @@ def _build_attestations_from_spec( signed_attestation = self._build_signed_attestation_from_spec( attestation_item, state, key_manager ) - attestations.append(signed_attestation.message) + # Reconstruct Attestation from SignedAttestation components + attestations.append( + Attestation( + validator_id=signed_attestation.validator_id, + data=signed_attestation.message, + ) + ) attestation_signatures.append(signed_attestation.signature) else: - attestations.append(attestation_item.message) + # Reconstruct Attestation from existing SignedAttestation + attestations.append( + Attestation( + validator_id=attestation_item.validator_id, + data=attestation_item.message, + ) + ) attestation_signatures.append(attestation_item.signature) return attestations, attestation_signatures @@ -313,7 +330,10 @@ def _build_signed_attestation_from_spec( # Sign the attestation - use dummy signature if expecting invalid signature if spec.valid_signature: # Generate valid signature using key manager - signature = key_manager.sign_attestation(attestation) + signature = key_manager.sign_attestation_data( + attestation.validator_id, + attestation.data, + ) else: # Generate an invalid dummy signature (all zeros) from lean_spec.subspecs.xmss.constants import TEST_CONFIG @@ -328,6 +348,7 @@ def _build_signed_attestation_from_spec( # Create signed attestation return SignedAttestation( - message=attestation, + validator_id=attestation.validator_id, + message=attestation.data, signature=signature, ) From 3b02587cdfd2bcd8ecffd320c8bdc6ffa19d3069 Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Thu, 11 Dec 2025 02:27:17 +0530 Subject: [PATCH 04/12] feat: aggregate attestations --- .../test_fixtures/state_transition.py | 9 +- .../containers/attestation/__init__.py | 10 +- .../containers/attestation/attestation.py | 93 ++++++++-- .../subspecs/containers/block/block.py | 44 +++-- .../subspecs/containers/state/state.py | 37 +++- src/lean_spec/subspecs/forkchoice/store.py | 28 +-- .../test_attestation_aggregation.py | 164 ++++++++++++++++++ .../forkchoice/test_store_attestations.py | 110 ++++++++++++ 8 files changed, 443 insertions(+), 52 deletions(-) create mode 100644 tests/lean_spec/subspecs/containers/test_attestation_aggregation.py create mode 100644 tests/lean_spec/subspecs/forkchoice/test_store_attestations.py 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 59441059..0db89a9e 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -5,7 +5,7 @@ from pydantic import ConfigDict, PrivateAttr, field_serializer from lean_spec.subspecs.containers.attestation import ( - aggregated_attestation_to_plain, + aggregated_attestations_to_plain, attestation_to_aggregated, ) from lean_spec.subspecs.containers.block.block import Block, BlockBody @@ -209,8 +209,13 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, parent_root = hash_tree_root(temp_state.latest_block_header) # Extract attestations from body if provided, converting from aggregated form + # Flatten all plain attestations from all aggregated attestations attestations = ( - [aggregated_attestation_to_plain(att) for att in spec.body.attestations] + [ + plain_att + for att in spec.body.attestations + for plain_att in aggregated_attestations_to_plain(att) + ] if spec.body else [] ) diff --git a/src/lean_spec/subspecs/containers/attestation/__init__.py b/src/lean_spec/subspecs/containers/attestation/__init__.py index b0e6bc2d..d124f11d 100644 --- a/src/lean_spec/subspecs/containers/attestation/__init__.py +++ b/src/lean_spec/subspecs/containers/attestation/__init__.py @@ -6,8 +6,9 @@ AttestationData, SignedAggregatedAttestations, SignedAttestation, - aggregated_attestation_to_plain, - aggregation_bits_to_validator_index, + aggregate_attestations_by_data, + aggregated_attestations_to_plain, + aggregation_bits_to_validator_indices, attestation_to_aggregated, ) from .types import AggregatedSignatures, AggregationBits @@ -20,7 +21,8 @@ "AggregatedAttestations", "AggregatedSignatures", "AggregationBits", - "aggregation_bits_to_validator_index", - "aggregated_attestation_to_plain", + "aggregate_attestations_by_data", + "aggregation_bits_to_validator_indices", + "aggregated_attestations_to_plain", "attestation_to_aggregated", ] diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index 406784e2..f73b27b7 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -12,6 +12,8 @@ doesn't do this yet. """ +from collections import defaultdict + from lean_spec.subspecs.containers.slot import Slot from lean_spec.types import Container, Uint64 @@ -92,29 +94,45 @@ class SignedAggregatedAttestations(Container): """ -def _aggregation_bits_to_validator_index(bits: AggregationBits) -> Uint64: +def aggregation_bits_to_validator_indices(bits: AggregationBits) -> list[Uint64]: """ - Extract the single validator index encoded in aggregation bits. + Extract all validator indices encoded in aggregation bits. + + Returns the list of all validators who participated in the aggregation, + sorted by validator index. + + Args: + bits: Aggregation bitlist with participating validators. - Current devnets only support naive aggregation where every attestation - includes exactly one participant. The bitlist therefore acts as a compact - encoding of the validator index. + Returns: + List of validator indices, sorted in ascending order. """ - participants = [index for index, bit in enumerate(bits) if bool(bit)] - if len(participants) != 1: - raise AssertionError("Aggregated attestation must reference exactly one validator") - return Uint64(participants[0]) + validator_indices = [Uint64(index) for index, bit in enumerate(bits) if bool(bit)] + if not validator_indices: + raise AssertionError("Aggregated attestation must reference at least one validator") + return validator_indices -def aggregation_bits_to_validator_index(bits: AggregationBits) -> Uint64: - """Public helper wrapper for extracting the validator index from bits.""" - return _aggregation_bits_to_validator_index(bits) +def aggregated_attestations_to_plain( + aggregated: AggregatedAttestations, +) -> list[Attestation]: + """ + Convert aggregated attestation to a list of plain Attestation containers. + + Extracts all participating validator indices from the aggregation bits + and creates individual Attestation objects for each validator. + Args: + aggregated: Aggregated attestation with one or more participating validators. -def aggregated_attestation_to_plain(aggregated: AggregatedAttestations) -> Attestation: - """Convert aggregated attestation data to the plain Attestation container.""" - validator_index = _aggregation_bits_to_validator_index(aggregated.aggregation_bits) - return Attestation(validator_id=validator_index, data=aggregated.data) + Returns: + List of plain attestations, one per participating validator. + """ + validator_indices = aggregation_bits_to_validator_indices(aggregated.aggregation_bits) + return [ + Attestation(validator_id=validator_id, data=aggregated.data) + for validator_id in validator_indices + ] def attestation_to_aggregated(attestation: Attestation) -> AggregatedAttestations: @@ -126,3 +144,46 @@ def attestation_to_aggregated(attestation: Attestation) -> AggregatedAttestation aggregation_bits=AggregationBits(data=bits), data=attestation.data, ) + + +def aggregate_attestations_by_data( + attestations: list[Attestation], +) -> list[AggregatedAttestations]: + """ + Aggregate attestations with common attestation data. + + Groups attestations by their AttestationData and creates one AggregatedAttestations + per unique data, with all participating validator bits set. + + Args: + attestations: List of attestations to aggregate. + + Returns: + List of aggregated attestations with proper bit aggregation. + """ + # Group validator IDs by attestation data (avoids intermediate objects) + data_to_validator_ids: dict[AttestationData, list[int]] = defaultdict(list) + + for attestation in attestations: + data_to_validator_ids[attestation.data].append(int(attestation.validator_id)) + + # Create aggregated attestations with all relevant bits set + result: list[AggregatedAttestations] = [] + + for data, validator_ids in data_to_validator_ids.items(): + # Find the maximum validator index to determine bitlist size + max_validator_id = max(validator_ids) + + # Create bitlist with all participating validators set to True + bits = [False] * (max_validator_id + 1) + for validator_id in validator_ids: + bits[validator_id] = True + + result.append( + AggregatedAttestations( + aggregation_bits=AggregationBits(data=bits), + data=data, + ) + ) + + return result diff --git a/src/lean_spec/subspecs/containers/block/block.py b/src/lean_spec/subspecs/containers/block/block.py index 17cc4144..2a504486 100644 --- a/src/lean_spec/subspecs/containers/block/block.py +++ b/src/lean_spec/subspecs/containers/block/block.py @@ -9,6 +9,7 @@ can propose. """ +from itertools import pairwise from typing import TYPE_CHECKING, cast from lean_spec.subspecs.containers.slot import Slot @@ -17,7 +18,7 @@ from lean_spec.types.container import Container from ...xmss.containers import Signature as XmssSignature -from ..attestation import Attestation, aggregation_bits_to_validator_index +from ..attestation import Attestation, AttestationData, aggregation_bits_to_validator_indices from ..validator import Validator from .types import AggregatedAttestationsList, AttestationSignatures @@ -159,29 +160,46 @@ def verify_signatures(self, parent_state: "State") -> bool: block_attestations = block.body.attestations attestation_signatures = signatures.attestation_signatures - # Verify signature count matches attestation count. - assert len(attestation_signatures) == len(block_attestations), ( - "Number of attestation signatures does not match attestation count" - ) - validators = parent_state.validators - # Verify each block body attestation signature - for aggregated_attestation, signature in zip( - block_attestations, attestation_signatures, strict=True - ): - validator_id = aggregation_bits_to_validator_index( + # Collect all validator IDs and their corresponding attestation data + # from the aggregated attestations + validator_attestations: list[tuple[Uint64, AttestationData]] = [] + + for aggregated_attestation in block_attestations: + validator_ids = aggregation_bits_to_validator_indices( aggregated_attestation.aggregation_bits ) + for validator_id in validator_ids: + validator_attestations.append((validator_id, aggregated_attestation.data)) + + # Sort by validator_id to match the sorted signature order + validator_attestations.sort(key=lambda x: x[0]) + # Verify signature count matches total validator count + assert len(attestation_signatures) == len(validator_attestations), ( + "Number of attestation signatures does not match validator count" + ) + + # Verify signatures are in strictly ascending order by validator_id + # This ensures the block builder properly sorted the signatures + assert all(prev[0] < curr[0] for prev, curr in pairwise(validator_attestations)), ( + "Attestation signatures must be in strictly ascending order by validator_id. " + f"Found: {[vid for vid, _ in validator_attestations]}" + ) + + # Verify each validator's attestation signature + for (validator_id, attestation_data), signature in zip( + validator_attestations, attestation_signatures, strict=True + ): # Ensure validator exists in the active set assert validator_id < Uint64(len(validators)), "Validator index out of range" validator = cast(Validator, validators[validator_id]) assert signature.verify( validator.get_pubkey(), - aggregated_attestation.data.slot, - bytes(hash_tree_root(aggregated_attestation.data)), + attestation_data.slot, + bytes(hash_tree_root(attestation_data)), ), "Attestation signature verification failed" # Verify proposer attestation signature diff --git a/src/lean_spec/subspecs/containers/state/state.py b/src/lean_spec/subspecs/containers/state/state.py index 07e9f8cf..3a658c7f 100644 --- a/src/lean_spec/subspecs/containers/state/state.py +++ b/src/lean_spec/subspecs/containers/state/state.py @@ -15,8 +15,8 @@ from ..attestation import ( Attestation, SignedAttestation, - aggregated_attestation_to_plain, - attestation_to_aggregated, + aggregate_attestations_by_data, + aggregated_attestations_to_plain, ) if TYPE_CHECKING: @@ -357,12 +357,26 @@ def process_block(self, block: Block) -> "State": ------- State A new state with the processed block. + + Raises: + ------ + AssertionError + If block contains duplicate AttestationData. """ # First process the block header. state = self.process_block_header(block) # Process justification attestations by converting aggregated payloads - attestations = [aggregated_attestation_to_plain(att) for att in block.body.attestations] + attestations: list[Attestation] = [] + attestations_data = set() + for aggregated_att in block.body.attestations: + # No partial aggregation is allowed. + if aggregated_att.data in attestations_data: + raise AssertionError("Block contains duplicate AttestationData") + + attestations_data.add(aggregated_att.data) + attestations.extend(aggregated_attestations_to_plain(aggregated_att)) + return state.process_attestations(attestations) def process_attestations( @@ -657,7 +671,7 @@ def build_block( state_root=Bytes32.zero(), body=BlockBody( attestations=AggregatedAttestationsList( - data=[attestation_to_aggregated(att) for att in attestations] + data=aggregate_attestations_by_data(attestations) ) ), ) @@ -701,7 +715,20 @@ def build_block( attestations.extend(new_attestations) signatures.extend(new_signatures) + # Sort attestations and signatures by validator_id to ensure + # consistent ordering for signature validation. + # Only sort if we have signatures (i.e., attestation collection was performed) + if signatures: + attestations_with_sigs = sorted( + zip(attestations, signatures, strict=True), key=lambda x: x[0].validator_id + ) + sorted_attestations = [att for att, _ in attestations_with_sigs] + sorted_signatures = [sig for _, sig in attestations_with_sigs] + else: + sorted_attestations = attestations + sorted_signatures = signatures + # Store the post state root in the block final_block = candidate_block.model_copy(update={"state_root": hash_tree_root(post_state)}) - return final_block, post_state, attestations, signatures + return final_block, post_state, sorted_attestations, sorted_signatures diff --git a/src/lean_spec/subspecs/forkchoice/store.py b/src/lean_spec/subspecs/forkchoice/store.py index 872b6f12..9bc058e2 100644 --- a/src/lean_spec/subspecs/forkchoice/store.py +++ b/src/lean_spec/subspecs/forkchoice/store.py @@ -30,7 +30,7 @@ SignedBlockWithAttestation, State, ) -from lean_spec.subspecs.containers.attestation import aggregation_bits_to_validator_index +from lean_spec.subspecs.containers.attestation import aggregated_attestations_to_plain from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.containers import Signature @@ -461,21 +461,25 @@ def on_block(self, signed_block_with_attestation: SignedBlockWithAttestation) -> block_attestations = signed_block_with_attestation.message.block.body.attestations attestation_signatures = signed_block_with_attestation.signature.attestation_signatures - assert len(block_attestations) == len(attestation_signatures), ( - "Attestation signature list must align with block attestations" - ) + # Expand aggregated attestations into individual validator attestations + plain_attestations = [ + plain_att + for aggregated_att in block_attestations + for plain_att in aggregated_attestations_to_plain(aggregated_att) + ] - for aggregated_attestation, signature in zip( - block_attestations, attestation_signatures, strict=True - ): - validator_id = aggregation_bits_to_validator_index( - aggregated_attestation.aggregation_bits - ) + # Sort attestations by validator_id to align with signature order + plain_attestations.sort(key=lambda att: att.validator_id) + + assert len(plain_attestations) == len(attestation_signatures), ( + "Attestation signature list must align with validator attestations" + ) + for attestation, signature in zip(plain_attestations, attestation_signatures, strict=True): store = store.on_attestation( signed_attestation=SignedAttestation( - validator_id=validator_id, - message=aggregated_attestation.data, + validator_id=attestation.validator_id, + message=attestation.data, signature=signature, ), is_from_block=True, diff --git a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py new file mode 100644 index 00000000..d5f96942 --- /dev/null +++ b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py @@ -0,0 +1,164 @@ +"""Tests for attestation aggregation and signature ordering.""" + +import pytest + +from lean_spec.subspecs.containers.attestation import ( + AggregationBits, + Attestation, + AttestationData, + aggregate_attestations_by_data, + aggregation_bits_to_validator_indices, +) +from lean_spec.subspecs.containers.block import ( + Block, + BlockBody, + BlockSignatures, + BlockWithAttestation, + SignedBlockWithAttestation, +) +from lean_spec.subspecs.containers.block.types import ( + AggregatedAttestationsList, + AttestationSignatures, +) +from lean_spec.subspecs.containers.checkpoint import Checkpoint +from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.containers.state import State +from lean_spec.subspecs.containers.validator import Validator +from lean_spec.subspecs.ssz.hash import hash_tree_root +from lean_spec.subspecs.xmss.containers import Signature +from lean_spec.types import Bytes32, Bytes52, Uint64 + + +class TestAttestationAggregation: + """Test proper attestation aggregation by common data.""" + + def test_reject_empty_aggregation_bits(self) -> None: + """Validate aggregated attestation must include at least one validator.""" + bits = AggregationBits(data=[False, False, False]) + with pytest.raises(AssertionError, match="at least one validator"): + aggregation_bits_to_validator_indices(bits) + + def test_aggregate_attestations_by_common_data(self) -> None: + """Test that attestations with same data are properly aggregated.""" + # Create three attestations with two having common data + att_data1 = AttestationData( + slot=Slot(5), + head=Checkpoint(root=Bytes32.zero(), slot=Slot(4)), + target=Checkpoint(root=Bytes32.zero(), slot=Slot(3)), + source=Checkpoint(root=Bytes32.zero(), slot=Slot(2)), + ) + att_data2 = AttestationData( + slot=Slot(6), + head=Checkpoint(root=Bytes32.zero(), slot=Slot(5)), + target=Checkpoint(root=Bytes32.zero(), slot=Slot(4)), + source=Checkpoint(root=Bytes32.zero(), slot=Slot(3)), + ) + + attestations = [ + Attestation(validator_id=Uint64(1), data=att_data1), + Attestation(validator_id=Uint64(3), data=att_data1), + Attestation(validator_id=Uint64(5), data=att_data2), + ] + + aggregated = aggregate_attestations_by_data(attestations) + + # Should have 2 aggregated attestations (one per unique data) + assert len(aggregated) == 2 + + # Find the aggregated attestation with att_data1 + agg1 = next(agg for agg in aggregated if agg.data == att_data1) + validator_ids1 = aggregation_bits_to_validator_indices(agg1.aggregation_bits) + + # Should contain validators 1 and 3 + assert set(validator_ids1) == {Uint64(1), Uint64(3)} + + # Find the aggregated attestation with att_data2 + agg2 = next(agg for agg in aggregated if agg.data == att_data2) + validator_ids2 = aggregation_bits_to_validator_indices(agg2.aggregation_bits) + + # Should contain only validator 5 + assert set(validator_ids2) == {Uint64(5)} + + def test_aggregate_attestations_sets_all_bits(self) -> None: + """Test that aggregation sets all validator bits correctly.""" + att_data = AttestationData( + slot=Slot(5), + head=Checkpoint(root=Bytes32.zero(), slot=Slot(4)), + target=Checkpoint(root=Bytes32.zero(), slot=Slot(3)), + source=Checkpoint(root=Bytes32.zero(), slot=Slot(2)), + ) + + attestations = [ + Attestation(validator_id=Uint64(2), data=att_data), + Attestation(validator_id=Uint64(7), data=att_data), + Attestation(validator_id=Uint64(10), data=att_data), + ] + + aggregated = aggregate_attestations_by_data(attestations) + + assert len(aggregated) == 1 + validator_ids = aggregation_bits_to_validator_indices(aggregated[0].aggregation_bits) + + # Should have all three validators + assert len(validator_ids) == 3 + assert set(validator_ids) == {Uint64(2), Uint64(7), Uint64(10)} + + def test_aggregate_empty_attestations(self) -> None: + """Test aggregation with no attestations.""" + aggregated = aggregate_attestations_by_data([]) + assert len(aggregated) == 0 + + def test_aggregate_single_attestation(self) -> None: + """Test aggregation with a single attestation.""" + att_data = AttestationData( + slot=Slot(5), + head=Checkpoint(root=Bytes32.zero(), slot=Slot(4)), + target=Checkpoint(root=Bytes32.zero(), slot=Slot(3)), + source=Checkpoint(root=Bytes32.zero(), slot=Slot(2)), + ) + + attestations = [Attestation(validator_id=Uint64(5), data=att_data)] + + aggregated = aggregate_attestations_by_data(attestations) + + assert len(aggregated) == 1 + validator_ids = aggregation_bits_to_validator_indices(aggregated[0].aggregation_bits) + assert validator_ids == [Uint64(5)] + + +class TestDuplicateAttestationDataValidation: + """Test validation that blocks don't contain duplicate AttestationData.""" + + def test_duplicate_attestation_data_detection(self) -> None: + """Ensure conversion to plain attestations preserves duplicates.""" + att_data = AttestationData( + slot=Slot(1), + head=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), + target=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), + source=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), + ) + + from lean_spec.subspecs.containers.attestation import AggregatedAttestations + from lean_spec.subspecs.containers.attestation.types import AggregationBits + + agg1 = AggregatedAttestations( + aggregation_bits=AggregationBits(data=[False, True]), + data=att_data, + ) + agg2 = AggregatedAttestations( + aggregation_bits=AggregationBits(data=[False, True, True]), + data=att_data, + ) + + from lean_spec.subspecs.containers.attestation import aggregated_attestations_to_plain + + plain = [ + plain_att + for aggregated in (agg1, agg2) + for plain_att in aggregated_attestations_to_plain(aggregated) + ] + + # Expect 2 plain attestations (because validator 1 is common in agg1 and agg2) + # validator 1 and validator 2 are the only unique validators in the attestations + assert len(set(plain)) == 2 + assert all(att.data == att_data for att in plain) diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py new file mode 100644 index 00000000..96379fa5 --- /dev/null +++ b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py @@ -0,0 +1,110 @@ +"""Tests for Store attestation handling.""" + +from consensus_testing.keys import XmssKeyManager + +from lean_spec.subspecs.chain.config import SECONDS_PER_SLOT +from lean_spec.subspecs.containers.attestation import ( + Attestation, + AttestationData, + SignedAttestation, +) +from lean_spec.subspecs.containers.block import ( + Block, + BlockBody, + BlockSignatures, + BlockWithAttestation, + SignedBlockWithAttestation, +) +from lean_spec.subspecs.containers.block.types import ( + AggregatedAttestationsList, + AttestationSignatures, +) +from lean_spec.subspecs.containers.checkpoint import Checkpoint +from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.containers.state import State, Validators +from lean_spec.subspecs.containers.validator import Validator +from lean_spec.subspecs.forkchoice import Store +from lean_spec.subspecs.ssz.hash import hash_tree_root +from lean_spec.types import Bytes32, Uint64 + + +def test_on_block_processes_multi_validator_aggregations() -> None: + """Ensure Store.on_block handles aggregated attestations with many validators.""" + key_manager = XmssKeyManager(max_slot=Slot(10)) + validators = Validators( + data=[ + Validator(pubkey=key_manager[Uint64(i)].public.encode_bytes(), index=Uint64(i)) + for i in range(3) + ] + ) + genesis_state = State.generate_genesis(genesis_time=Uint64(0), validators=validators) + genesis_block = Block( + slot=Slot(0), + proposer_index=Uint64(0), + parent_root=Bytes32.zero(), + state_root=hash_tree_root(genesis_state), + body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + ) + + base_store = Store.get_forkchoice_store(genesis_state, genesis_block) + consumer_store = base_store + + # Producer view knows about attestations from validators 1 and 2 + attestation_slot = Slot(1) + attestation_data = base_store.produce_attestation_data(attestation_slot) + signed_attestations = { + validator_id: SignedAttestation( + validator_id=validator_id, + message=attestation_data, + signature=key_manager.sign_attestation_data(validator_id, attestation_data), + ) + for validator_id in (Uint64(1), Uint64(2)) + } + producer_store = base_store.model_copy( + update={"latest_known_attestations": signed_attestations} + ) + + # For slot 1 with 3 validators: 1 % 3 == 1, so validator 1 is the proposer + proposer_index = Uint64(1) + _, block, attestation_signatures = producer_store.produce_block_with_signatures( + attestation_slot, + proposer_index, + ) + + block_root = hash_tree_root(block) + parent_state = producer_store.states[block.parent_root] + proposer_attestation = Attestation( + validator_id=proposer_index, + data=AttestationData( + slot=attestation_slot, + head=Checkpoint(root=block_root, slot=attestation_slot), + target=Checkpoint(root=block_root, slot=attestation_slot), + source=Checkpoint(root=block.parent_root, slot=parent_state.latest_block_header.slot), + ), + ) + proposer_signature = key_manager.sign_attestation_data( + proposer_attestation.validator_id, + proposer_attestation.data, + ) + + signed_block = SignedBlockWithAttestation( + message=BlockWithAttestation( + block=block, + proposer_attestation=proposer_attestation, + ), + signature=BlockSignatures( + attestation_signatures=AttestationSignatures(data=attestation_signatures), + proposer_signature=proposer_signature, + ), + ) + + # Advance consumer store time to block's slot before processing + block_time = consumer_store.config.genesis_time + block.slot * Uint64(SECONDS_PER_SLOT) + consumer_store = consumer_store.on_tick(block_time, has_proposal=True) + + updated_store = consumer_store.on_block(signed_block) + + assert Uint64(1) in updated_store.latest_known_attestations + assert Uint64(2) in updated_store.latest_known_attestations + assert updated_store.latest_known_attestations[Uint64(1)].message == attestation_data + assert updated_store.latest_known_attestations[Uint64(2)].message == attestation_data From bc355133a10f168ccd96ffa6faba15a5395d6b81 Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Thu, 11 Dec 2025 13:28:38 +0530 Subject: [PATCH 05/12] update: change AggregatedAttestations to AggregatedAttestation --- .../test_fixtures/fork_choice.py | 4 ++-- .../test_fixtures/state_transition.py | 6 ++--- src/lean_spec/subspecs/containers/__init__.py | 4 ++-- .../containers/attestation/__init__.py | 4 ++-- .../containers/attestation/attestation.py | 18 +++++++-------- .../subspecs/containers/block/__init__.py | 4 ++-- .../subspecs/containers/block/block.py | 4 ++-- .../subspecs/containers/block/types.py | 6 ++--- .../subspecs/containers/state/state.py | 6 ++--- .../devnet/state_transition/test_genesis.py | 16 ++++++------- .../test_attestation_aggregation.py | 23 ++++--------------- .../lean_spec/subspecs/forkchoice/conftest.py | 4 ++-- .../forkchoice/test_store_attestations.py | 4 ++-- .../forkchoice/test_time_management.py | 6 ++--- .../subspecs/forkchoice/test_validator.py | 6 ++--- tests/lean_spec/subspecs/ssz/test_block.py | 4 ++-- 16 files changed, 52 insertions(+), 67 deletions(-) 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 f81cacd7..c1b97709 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py +++ b/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py @@ -21,7 +21,7 @@ SignedBlockWithAttestation, ) from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestationsList, + AggregatedAttestationList, AttestationSignatures, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint @@ -137,7 +137,7 @@ def set_anchor_block_default(self) -> ForkChoiceTest: proposer_index=self.anchor_state.latest_block_header.proposer_index, parent_root=self.anchor_state.latest_block_header.parent_root, state_root=hash_tree_root(self.anchor_state), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) return self 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 0db89a9e..aff06308 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -9,7 +9,7 @@ attestation_to_aggregated, ) from lean_spec.subspecs.containers.block.block import Block, BlockBody -from lean_spec.subspecs.containers.block.types import AggregatedAttestationsList +from lean_spec.subspecs.containers.block.types import AggregatedAttestationList from lean_spec.subspecs.containers.state.state import State from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Uint64 @@ -227,7 +227,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, proposer_index=proposer_index, parent_root=parent_root, state_root=spec.state_root, - body=spec.body or BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=spec.body or BlockBody(attestations=AggregatedAttestationList(data=[])), ) return block, None @@ -240,7 +240,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, state_root=Bytes32.zero(), body=spec.body or BlockBody( - attestations=AggregatedAttestationsList( + attestations=AggregatedAttestationList( data=[attestation_to_aggregated(att) for att in attestations] ) ), diff --git a/src/lean_spec/subspecs/containers/__init__.py b/src/lean_spec/subspecs/containers/__init__.py index 4548fef5..0ac92048 100644 --- a/src/lean_spec/subspecs/containers/__init__.py +++ b/src/lean_spec/subspecs/containers/__init__.py @@ -9,7 +9,7 @@ """ from .attestation import ( - AggregatedAttestations, + AggregatedAttestation, AggregatedSignatures, AggregationBits, Attestation, @@ -30,7 +30,7 @@ from .validator import Validator __all__ = [ - "AggregatedAttestations", + "AggregatedAttestation", "AggregatedSignatures", "AggregationBits", "AttestationData", diff --git a/src/lean_spec/subspecs/containers/attestation/__init__.py b/src/lean_spec/subspecs/containers/attestation/__init__.py index d124f11d..7cf74651 100644 --- a/src/lean_spec/subspecs/containers/attestation/__init__.py +++ b/src/lean_spec/subspecs/containers/attestation/__init__.py @@ -1,7 +1,7 @@ """Attestation containers and related types for the Lean spec.""" from .attestation import ( - AggregatedAttestations, + AggregatedAttestation, Attestation, AttestationData, SignedAggregatedAttestations, @@ -18,7 +18,7 @@ "Attestation", "SignedAttestation", "SignedAggregatedAttestations", - "AggregatedAttestations", + "AggregatedAttestation", "AggregatedSignatures", "AggregationBits", "aggregate_attestations_by_data", diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index f73b27b7..565ce506 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -61,7 +61,7 @@ class SignedAttestation(Container): """Signature aggregation produced by the leanVM (SNARKs in the future).""" -class AggregatedAttestations(Container): +class AggregatedAttestation(Container): """Aggregated attestation consisting of participation bits and message.""" aggregation_bits: AggregationBits @@ -78,7 +78,7 @@ class AggregatedAttestations(Container): class SignedAggregatedAttestations(Container): """Aggregated attestation bundled with aggregated signatures.""" - message: AggregatedAttestations + message: AggregatedAttestation """Aggregated attestation data.""" signature: AggregatedSignatures @@ -114,7 +114,7 @@ def aggregation_bits_to_validator_indices(bits: AggregationBits) -> list[Uint64] def aggregated_attestations_to_plain( - aggregated: AggregatedAttestations, + aggregated: AggregatedAttestation, ) -> list[Attestation]: """ Convert aggregated attestation to a list of plain Attestation containers. @@ -135,12 +135,12 @@ def aggregated_attestations_to_plain( ] -def attestation_to_aggregated(attestation: Attestation) -> AggregatedAttestations: +def attestation_to_aggregated(attestation: Attestation) -> AggregatedAttestation: """Convert a plain Attestation into the aggregated representation.""" validator_index = int(attestation.validator_id) bits = [False] * (validator_index + 1) bits[validator_index] = True - return AggregatedAttestations( + return AggregatedAttestation( aggregation_bits=AggregationBits(data=bits), data=attestation.data, ) @@ -148,11 +148,11 @@ def attestation_to_aggregated(attestation: Attestation) -> AggregatedAttestation def aggregate_attestations_by_data( attestations: list[Attestation], -) -> list[AggregatedAttestations]: +) -> list[AggregatedAttestation]: """ Aggregate attestations with common attestation data. - Groups attestations by their AttestationData and creates one AggregatedAttestations + Groups attestations by their AttestationData and creates one AggregatedAttestation per unique data, with all participating validator bits set. Args: @@ -168,7 +168,7 @@ def aggregate_attestations_by_data( data_to_validator_ids[attestation.data].append(int(attestation.validator_id)) # Create aggregated attestations with all relevant bits set - result: list[AggregatedAttestations] = [] + result: list[AggregatedAttestation] = [] for data, validator_ids in data_to_validator_ids.items(): # Find the maximum validator index to determine bitlist size @@ -180,7 +180,7 @@ def aggregate_attestations_by_data( bits[validator_id] = True result.append( - AggregatedAttestations( + AggregatedAttestation( aggregation_bits=AggregationBits(data=bits), data=data, ) diff --git a/src/lean_spec/subspecs/containers/block/__init__.py b/src/lean_spec/subspecs/containers/block/__init__.py index d036e142..2c664ce7 100644 --- a/src/lean_spec/subspecs/containers/block/__init__.py +++ b/src/lean_spec/subspecs/containers/block/__init__.py @@ -8,7 +8,7 @@ BlockWithAttestation, SignedBlockWithAttestation, ) -from .types import AggregatedAttestationsList, AttestationSignatures +from .types import AggregatedAttestationList, AttestationSignatures __all__ = [ "Block", @@ -17,6 +17,6 @@ "BlockSignatures", "BlockWithAttestation", "SignedBlockWithAttestation", - "AggregatedAttestationsList", + "AggregatedAttestationList", "AttestationSignatures", ] diff --git a/src/lean_spec/subspecs/containers/block/block.py b/src/lean_spec/subspecs/containers/block/block.py index 2a504486..ee206a23 100644 --- a/src/lean_spec/subspecs/containers/block/block.py +++ b/src/lean_spec/subspecs/containers/block/block.py @@ -20,7 +20,7 @@ from ...xmss.containers import Signature as XmssSignature from ..attestation import Attestation, AttestationData, aggregation_bits_to_validator_indices from ..validator import Validator -from .types import AggregatedAttestationsList, AttestationSignatures +from .types import AggregatedAttestationList, AttestationSignatures if TYPE_CHECKING: from ..state import State @@ -34,7 +34,7 @@ class BlockBody(Container): packaged into blocks. """ - attestations: AggregatedAttestationsList + attestations: AggregatedAttestationList """Plain validator attestations carried in the block body. Individual signatures live in the aggregated block signature list, so diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index 08a472ce..c5c71c72 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -4,13 +4,13 @@ from ...chain.config import VALIDATOR_REGISTRY_LIMIT from ...xmss.containers import Signature -from ..attestation import AggregatedAttestations +from ..attestation import AggregatedAttestation -class AggregatedAttestationsList(SSZList): +class AggregatedAttestationList(SSZList): """List of aggregated attestations included in a block.""" - ELEMENT_TYPE = AggregatedAttestations + ELEMENT_TYPE = AggregatedAttestation LIMIT = int(VALIDATOR_REGISTRY_LIMIT) diff --git a/src/lean_spec/subspecs/containers/state/state.py b/src/lean_spec/subspecs/containers/state/state.py index 3a658c7f..229846b5 100644 --- a/src/lean_spec/subspecs/containers/state/state.py +++ b/src/lean_spec/subspecs/containers/state/state.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from lean_spec.subspecs.xmss.containers import Signature from ..block import Block, BlockBody, BlockHeader -from ..block.types import AggregatedAttestationsList +from ..block.types import AggregatedAttestationList from ..checkpoint import Checkpoint from ..config import Config from ..slot import Slot @@ -101,7 +101,7 @@ def generate_genesis(cls, genesis_time: Uint64, validators: Validators) -> "Stat proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationsList(data=[]))), + body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationList(data=[]))), ) # Assemble and return the full genesis state. @@ -670,7 +670,7 @@ def build_block( parent_root=parent_root, state_root=Bytes32.zero(), body=BlockBody( - attestations=AggregatedAttestationsList( + attestations=AggregatedAttestationList( data=aggregate_attestations_by_data(attestations) ) ), diff --git a/tests/consensus/devnet/state_transition/test_genesis.py b/tests/consensus/devnet/state_transition/test_genesis.py index 2f961b86..5d6e9b10 100644 --- a/tests/consensus/devnet/state_transition/test_genesis.py +++ b/tests/consensus/devnet/state_transition/test_genesis.py @@ -11,7 +11,7 @@ from consensus_testing import StateExpectation, StateTransitionTestFiller, generate_pre_state from lean_spec.subspecs.containers.block import Block, BlockBody -from lean_spec.subspecs.containers.block.types import AggregatedAttestationsList +from lean_spec.subspecs.containers.block.types import AggregatedAttestationList from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import State, Validators from lean_spec.subspecs.containers.state.types import ( @@ -55,7 +55,7 @@ def test_genesis_default_configuration( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=AggregatedAttestationsList(data=[])) + BlockBody(attestations=AggregatedAttestationList(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -100,7 +100,7 @@ def test_genesis_custom_time( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=AggregatedAttestationsList(data=[])) + BlockBody(attestations=AggregatedAttestationList(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -143,7 +143,7 @@ def test_genesis_custom_validator_set( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=AggregatedAttestationsList(data=[])) + BlockBody(attestations=AggregatedAttestationList(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -173,7 +173,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state1), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) # Compute hash of first genesis block @@ -190,7 +190,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state1_copy), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) genesis_block_hash1_copy = hash_tree_root(genesis_block1_copy) @@ -215,7 +215,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state2), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) genesis_block_hash2 = hash_tree_root(genesis_block2) @@ -240,7 +240,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state3), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) genesis_block_hash3 = hash_tree_root(genesis_block3) diff --git a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py index d5f96942..369939dc 100644 --- a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py +++ b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py @@ -9,24 +9,9 @@ aggregate_attestations_by_data, aggregation_bits_to_validator_indices, ) -from lean_spec.subspecs.containers.block import ( - Block, - BlockBody, - BlockSignatures, - BlockWithAttestation, - SignedBlockWithAttestation, -) -from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestationsList, - AttestationSignatures, -) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.containers.state import State -from lean_spec.subspecs.containers.validator import Validator -from lean_spec.subspecs.ssz.hash import hash_tree_root -from lean_spec.subspecs.xmss.containers import Signature -from lean_spec.types import Bytes32, Bytes52, Uint64 +from lean_spec.types import Bytes32, Uint64 class TestAttestationAggregation: @@ -138,14 +123,14 @@ def test_duplicate_attestation_data_detection(self) -> None: source=Checkpoint(root=Bytes32.zero(), slot=Slot(0)), ) - from lean_spec.subspecs.containers.attestation import AggregatedAttestations + from lean_spec.subspecs.containers.attestation import AggregatedAttestation from lean_spec.subspecs.containers.attestation.types import AggregationBits - agg1 = AggregatedAttestations( + agg1 = AggregatedAttestation( aggregation_bits=AggregationBits(data=[False, True]), data=att_data, ) - agg2 = AggregatedAttestations( + agg2 = AggregatedAttestation( aggregation_bits=AggregationBits(data=[False, True, True]), data=att_data, ) diff --git a/tests/lean_spec/subspecs/forkchoice/conftest.py b/tests/lean_spec/subspecs/forkchoice/conftest.py index 454edd98..b466407a 100644 --- a/tests/lean_spec/subspecs/forkchoice/conftest.py +++ b/tests/lean_spec/subspecs/forkchoice/conftest.py @@ -11,7 +11,7 @@ SignedAttestation, State, ) -from lean_spec.subspecs.containers.block import AggregatedAttestationsList, BlockHeader +from lean_spec.subspecs.containers.block import AggregatedAttestationList, BlockHeader from lean_spec.subspecs.containers.config import Config from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators @@ -44,7 +44,7 @@ def __init__(self, latest_justified: Checkpoint) -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationsList(data=[]))), + body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationList(data=[]))), ) super().__init__( diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py index 96379fa5..792f821c 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py +++ b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py @@ -16,7 +16,7 @@ SignedBlockWithAttestation, ) from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestationsList, + AggregatedAttestationList, AttestationSignatures, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint @@ -43,7 +43,7 @@ def test_on_block_processes_multi_validator_aggregations() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) base_store = Store.get_forkchoice_store(genesis_state, genesis_block) diff --git a/tests/lean_spec/subspecs/forkchoice/test_time_management.py b/tests/lean_spec/subspecs/forkchoice/test_time_management.py index 66a8a49e..32a0b9d8 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_time_management.py +++ b/tests/lean_spec/subspecs/forkchoice/test_time_management.py @@ -10,7 +10,7 @@ State, Validator, ) -from lean_spec.subspecs.containers.block import AggregatedAttestationsList +from lean_spec.subspecs.containers.block import AggregatedAttestationList from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators from lean_spec.subspecs.forkchoice import Store @@ -35,7 +35,7 @@ def sample_store(sample_config: Config) -> Store: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32(b"state" + b"\x00" * 27), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) genesis_hash = hash_tree_root(genesis_block) @@ -306,7 +306,7 @@ def test_get_proposal_head_basic(self, sample_store: Store) -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32(b"genesis" + b"\x00" * 25), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) genesis_hash = hash_tree_root(genesis_block) diff --git a/tests/lean_spec/subspecs/forkchoice/test_validator.py b/tests/lean_spec/subspecs/forkchoice/test_validator.py index 3470895f..d0e31d0d 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_validator.py +++ b/tests/lean_spec/subspecs/forkchoice/test_validator.py @@ -14,7 +14,7 @@ State, Validator, ) -from lean_spec.subspecs.containers.block import AggregatedAttestationsList +from lean_spec.subspecs.containers.block import AggregatedAttestationList from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import ( HistoricalBlockHashes, @@ -82,7 +82,7 @@ def sample_store(config: Config, sample_state: State) -> Store: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(sample_state), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ) genesis_hash = hash_tree_root(genesis_block) @@ -515,7 +515,7 @@ def test_validator_operations_empty_store(self) -> None: config = Config(genesis_time=Uint64(1000)) # Create minimal genesis block first - genesis_body = BlockBody(attestations=AggregatedAttestationsList(data=[])) + genesis_body = BlockBody(attestations=AggregatedAttestationList(data=[])) # Create validators list with 3 validators validators = Validators( diff --git a/tests/lean_spec/subspecs/ssz/test_block.py b/tests/lean_spec/subspecs/ssz/test_block.py index bf38a0d0..7ca92814 100644 --- a/tests/lean_spec/subspecs/ssz/test_block.py +++ b/tests/lean_spec/subspecs/ssz/test_block.py @@ -7,7 +7,7 @@ SignedBlockWithAttestation, ) from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestationsList, + AggregatedAttestationList, AttestationSignatures, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint @@ -26,7 +26,7 @@ def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body=BlockBody(attestations=AggregatedAttestationsList(data=[])), + body=BlockBody(attestations=AggregatedAttestationList(data=[])), ), proposer_attestation=Attestation( validator_id=Uint64(0), From 2ea894693217f704cc466f6f6f8bdbfddf01608d Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Fri, 12 Dec 2025 16:07:14 +0530 Subject: [PATCH 06/12] refactor: variable naming --- .../test_fixtures/fork_choice.py | 8 ++++---- .../test_fixtures/state_transition.py | 6 +++--- .../test_fixtures/verify_signatures.py | 4 ++-- .../subspecs/containers/block/__init__.py | 6 +++--- src/lean_spec/subspecs/containers/block/block.py | 6 +++--- src/lean_spec/subspecs/containers/block/types.py | 8 ++++---- src/lean_spec/subspecs/containers/state/state.py | 6 +++--- .../devnet/state_transition/test_genesis.py | 16 ++++++++-------- tests/lean_spec/subspecs/forkchoice/conftest.py | 4 ++-- .../forkchoice/test_store_attestations.py | 8 ++++---- .../subspecs/forkchoice/test_time_management.py | 6 +++--- .../subspecs/forkchoice/test_validator.py | 6 +++--- tests/lean_spec/subspecs/ssz/test_block.py | 8 ++++---- 13 files changed, 46 insertions(+), 46 deletions(-) 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 c1b97709..21fa9e47 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py +++ b/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py @@ -21,8 +21,8 @@ SignedBlockWithAttestation, ) from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestationList, - AttestationSignatures, + AggregatedAttestations, + NaiveAggregatedSignature, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot @@ -137,7 +137,7 @@ def set_anchor_block_default(self) -> ForkChoiceTest: proposer_index=self.anchor_state.latest_block_header.proposer_index, parent_root=self.anchor_state.latest_block_header.parent_root, state_root=hash_tree_root(self.anchor_state), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) return self @@ -362,7 +362,7 @@ def _build_block_from_spec( proposer_attestation=proposer_attestation, ), signature=BlockSignatures( - attestation_signatures=AttestationSignatures(data=attestation_signatures), + attestation_signatures=NaiveAggregatedSignature(data=attestation_signatures), proposer_signature=proposer_signature, ), ) 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 aff06308..afc58115 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -9,7 +9,7 @@ attestation_to_aggregated, ) from lean_spec.subspecs.containers.block.block import Block, BlockBody -from lean_spec.subspecs.containers.block.types import AggregatedAttestationList +from lean_spec.subspecs.containers.block.types import AggregatedAttestations from lean_spec.subspecs.containers.state.state import State from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Uint64 @@ -227,7 +227,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, proposer_index=proposer_index, parent_root=parent_root, state_root=spec.state_root, - body=spec.body or BlockBody(attestations=AggregatedAttestationList(data=[])), + body=spec.body or BlockBody(attestations=AggregatedAttestations(data=[])), ) return block, None @@ -240,7 +240,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, state_root=Bytes32.zero(), body=spec.body or BlockBody( - attestations=AggregatedAttestationList( + attestations=AggregatedAttestations( data=[attestation_to_aggregated(att) for att in attestations] ) ), 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 348c2500..6517a458 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -17,7 +17,7 @@ BlockWithAttestation, SignedBlockWithAttestation, ) -from lean_spec.subspecs.containers.block.types import AttestationSignatures +from lean_spec.subspecs.containers.block.types import NaiveAggregatedSignature from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state.state import State @@ -239,7 +239,7 @@ def _build_block_from_spec( proposer_attestation=proposer_attestation, ), signature=BlockSignatures( - attestation_signatures=AttestationSignatures(data=signatures), + attestation_signatures=NaiveAggregatedSignature(data=signatures), proposer_signature=proposer_attestation_signature, ), ) diff --git a/src/lean_spec/subspecs/containers/block/__init__.py b/src/lean_spec/subspecs/containers/block/__init__.py index 2c664ce7..ffadc676 100644 --- a/src/lean_spec/subspecs/containers/block/__init__.py +++ b/src/lean_spec/subspecs/containers/block/__init__.py @@ -8,7 +8,7 @@ BlockWithAttestation, SignedBlockWithAttestation, ) -from .types import AggregatedAttestationList, AttestationSignatures +from .types import AggregatedAttestations, NaiveAggregatedSignature __all__ = [ "Block", @@ -17,6 +17,6 @@ "BlockSignatures", "BlockWithAttestation", "SignedBlockWithAttestation", - "AggregatedAttestationList", - "AttestationSignatures", + "AggregatedAttestations", + "NaiveAggregatedSignature", ] diff --git a/src/lean_spec/subspecs/containers/block/block.py b/src/lean_spec/subspecs/containers/block/block.py index ee206a23..92103460 100644 --- a/src/lean_spec/subspecs/containers/block/block.py +++ b/src/lean_spec/subspecs/containers/block/block.py @@ -20,7 +20,7 @@ from ...xmss.containers import Signature as XmssSignature from ..attestation import Attestation, AttestationData, aggregation_bits_to_validator_indices from ..validator import Validator -from .types import AggregatedAttestationList, AttestationSignatures +from .types import AggregatedAttestations, NaiveAggregatedSignature if TYPE_CHECKING: from ..state import State @@ -34,7 +34,7 @@ class BlockBody(Container): packaged into blocks. """ - attestations: AggregatedAttestationList + attestations: AggregatedAttestations """Plain validator attestations carried in the block body. Individual signatures live in the aggregated block signature list, so @@ -102,7 +102,7 @@ class BlockWithAttestation(Container): class BlockSignatures(Container): """Signature payload for the block.""" - attestation_signatures: AttestationSignatures + attestation_signatures: NaiveAggregatedSignature """Signatures for the attestations in the block body. Contains a naive list of signatures for the attestations in the block body. diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index c5c71c72..2fde6c10 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -3,19 +3,19 @@ from lean_spec.types import SSZList from ...chain.config import VALIDATOR_REGISTRY_LIMIT -from ...xmss.containers import Signature +from ...xmss.containers import Signature as XmssSignature from ..attestation import AggregatedAttestation -class AggregatedAttestationList(SSZList): +class AggregatedAttestations(SSZList): """List of aggregated attestations included in a block.""" ELEMENT_TYPE = AggregatedAttestation LIMIT = int(VALIDATOR_REGISTRY_LIMIT) -class AttestationSignatures(SSZList): +class NaiveAggregatedSignature(SSZList): """Aggregated signature list included alongside the block proposer's attestation.""" - ELEMENT_TYPE = Signature + ELEMENT_TYPE = XmssSignature LIMIT = int(VALIDATOR_REGISTRY_LIMIT) diff --git a/src/lean_spec/subspecs/containers/state/state.py b/src/lean_spec/subspecs/containers/state/state.py index 229846b5..cf659ba5 100644 --- a/src/lean_spec/subspecs/containers/state/state.py +++ b/src/lean_spec/subspecs/containers/state/state.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from lean_spec.subspecs.xmss.containers import Signature from ..block import Block, BlockBody, BlockHeader -from ..block.types import AggregatedAttestationList +from ..block.types import AggregatedAttestations from ..checkpoint import Checkpoint from ..config import Config from ..slot import Slot @@ -101,7 +101,7 @@ def generate_genesis(cls, genesis_time: Uint64, validators: Validators) -> "Stat proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationList(data=[]))), + body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestations(data=[]))), ) # Assemble and return the full genesis state. @@ -670,7 +670,7 @@ def build_block( parent_root=parent_root, state_root=Bytes32.zero(), body=BlockBody( - attestations=AggregatedAttestationList( + attestations=AggregatedAttestations( data=aggregate_attestations_by_data(attestations) ) ), diff --git a/tests/consensus/devnet/state_transition/test_genesis.py b/tests/consensus/devnet/state_transition/test_genesis.py index 5d6e9b10..5e761711 100644 --- a/tests/consensus/devnet/state_transition/test_genesis.py +++ b/tests/consensus/devnet/state_transition/test_genesis.py @@ -11,7 +11,7 @@ from consensus_testing import StateExpectation, StateTransitionTestFiller, generate_pre_state from lean_spec.subspecs.containers.block import Block, BlockBody -from lean_spec.subspecs.containers.block.types import AggregatedAttestationList +from lean_spec.subspecs.containers.block.types import AggregatedAttestations from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import State, Validators from lean_spec.subspecs.containers.state.types import ( @@ -55,7 +55,7 @@ def test_genesis_default_configuration( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=AggregatedAttestationList(data=[])) + BlockBody(attestations=AggregatedAttestations(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -100,7 +100,7 @@ def test_genesis_custom_time( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=AggregatedAttestationList(data=[])) + BlockBody(attestations=AggregatedAttestations(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -143,7 +143,7 @@ def test_genesis_custom_validator_set( latest_block_header_parent_root=Bytes32.zero(), latest_block_header_state_root=Bytes32.zero(), latest_block_header_body_root=hash_tree_root( - BlockBody(attestations=AggregatedAttestationList(data=[])) + BlockBody(attestations=AggregatedAttestations(data=[])) ), historical_block_hashes=HistoricalBlockHashes(data=[]), justified_slots=JustifiedSlots(data=[]), @@ -173,7 +173,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state1), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) # Compute hash of first genesis block @@ -190,7 +190,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state1_copy), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) genesis_block_hash1_copy = hash_tree_root(genesis_block1_copy) @@ -215,7 +215,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state2), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) genesis_block_hash2 = hash_tree_root(genesis_block2) @@ -240,7 +240,7 @@ def test_genesis_block_hash_comparison() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state3), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) genesis_block_hash3 = hash_tree_root(genesis_block3) diff --git a/tests/lean_spec/subspecs/forkchoice/conftest.py b/tests/lean_spec/subspecs/forkchoice/conftest.py index b466407a..9750cf0a 100644 --- a/tests/lean_spec/subspecs/forkchoice/conftest.py +++ b/tests/lean_spec/subspecs/forkchoice/conftest.py @@ -11,7 +11,7 @@ SignedAttestation, State, ) -from lean_spec.subspecs.containers.block import AggregatedAttestationList, BlockHeader +from lean_spec.subspecs.containers.block import AggregatedAttestations, BlockHeader from lean_spec.subspecs.containers.config import Config from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators @@ -44,7 +44,7 @@ def __init__(self, latest_justified: Checkpoint) -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestationList(data=[]))), + body_root=hash_tree_root(BlockBody(attestations=AggregatedAttestations(data=[]))), ) super().__init__( diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py index 792f821c..b1c9e1a8 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py +++ b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py @@ -16,8 +16,8 @@ SignedBlockWithAttestation, ) from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestationList, - AttestationSignatures, + AggregatedAttestations, + NaiveAggregatedSignature, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot @@ -43,7 +43,7 @@ def test_on_block_processes_multi_validator_aggregations() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(genesis_state), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) base_store = Store.get_forkchoice_store(genesis_state, genesis_block) @@ -93,7 +93,7 @@ def test_on_block_processes_multi_validator_aggregations() -> None: proposer_attestation=proposer_attestation, ), signature=BlockSignatures( - attestation_signatures=AttestationSignatures(data=attestation_signatures), + attestation_signatures=NaiveAggregatedSignature(data=attestation_signatures), proposer_signature=proposer_signature, ), ) diff --git a/tests/lean_spec/subspecs/forkchoice/test_time_management.py b/tests/lean_spec/subspecs/forkchoice/test_time_management.py index 32a0b9d8..509f9399 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_time_management.py +++ b/tests/lean_spec/subspecs/forkchoice/test_time_management.py @@ -10,7 +10,7 @@ State, Validator, ) -from lean_spec.subspecs.containers.block import AggregatedAttestationList +from lean_spec.subspecs.containers.block import AggregatedAttestations from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import Validators from lean_spec.subspecs.forkchoice import Store @@ -35,7 +35,7 @@ def sample_store(sample_config: Config) -> Store: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32(b"state" + b"\x00" * 27), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) genesis_hash = hash_tree_root(genesis_block) @@ -306,7 +306,7 @@ def test_get_proposal_head_basic(self, sample_store: Store) -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32(b"genesis" + b"\x00" * 25), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) genesis_hash = hash_tree_root(genesis_block) diff --git a/tests/lean_spec/subspecs/forkchoice/test_validator.py b/tests/lean_spec/subspecs/forkchoice/test_validator.py index d0e31d0d..400ca09f 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_validator.py +++ b/tests/lean_spec/subspecs/forkchoice/test_validator.py @@ -14,7 +14,7 @@ State, Validator, ) -from lean_spec.subspecs.containers.block import AggregatedAttestationList +from lean_spec.subspecs.containers.block import AggregatedAttestations from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state import ( HistoricalBlockHashes, @@ -82,7 +82,7 @@ def sample_store(config: Config, sample_state: State) -> Store: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=hash_tree_root(sample_state), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ) genesis_hash = hash_tree_root(genesis_block) @@ -515,7 +515,7 @@ def test_validator_operations_empty_store(self) -> None: config = Config(genesis_time=Uint64(1000)) # Create minimal genesis block first - genesis_body = BlockBody(attestations=AggregatedAttestationList(data=[])) + genesis_body = BlockBody(attestations=AggregatedAttestations(data=[])) # Create validators list with 3 validators validators = Validators( diff --git a/tests/lean_spec/subspecs/ssz/test_block.py b/tests/lean_spec/subspecs/ssz/test_block.py index 7ca92814..6423bf36 100644 --- a/tests/lean_spec/subspecs/ssz/test_block.py +++ b/tests/lean_spec/subspecs/ssz/test_block.py @@ -7,8 +7,8 @@ SignedBlockWithAttestation, ) from lean_spec.subspecs.containers.block.types import ( - AggregatedAttestationList, - AttestationSignatures, + AggregatedAttestations, + NaiveAggregatedSignature, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.koalabear import Fp @@ -26,7 +26,7 @@ def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: proposer_index=Uint64(0), parent_root=Bytes32.zero(), state_root=Bytes32.zero(), - body=BlockBody(attestations=AggregatedAttestationList(data=[])), + body=BlockBody(attestations=AggregatedAttestations(data=[])), ), proposer_attestation=Attestation( validator_id=Uint64(0), @@ -39,7 +39,7 @@ def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: ), ), signature=BlockSignatures( - attestation_signatures=AttestationSignatures(data=[]), + attestation_signatures=NaiveAggregatedSignature(data=[]), proposer_signature=Signature( path=HashTreeOpening(siblings=HashDigestList(data=[])), rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), From e8225fd6b9765920611247ef75b6e6badc208d47 Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Sat, 13 Dec 2025 05:05:51 +0530 Subject: [PATCH 07/12] feat: aggregate signature as list of naive signatures --- .../testing/src/consensus_testing/keys.py | 58 +++++++ .../test_fixtures/fork_choice.py | 10 +- .../test_fixtures/state_transition.py | 12 +- .../test_fixtures/verify_signatures.py | 22 ++- .../containers/attestation/__init__.py | 8 - .../containers/attestation/attestation.py | 148 +++++++----------- .../subspecs/containers/attestation/types.py | 50 +++++- .../subspecs/containers/block/__init__.py | 9 +- .../subspecs/containers/block/block.py | 75 ++++----- .../subspecs/containers/block/types.py | 9 +- .../subspecs/containers/state/state.py | 24 +-- src/lean_spec/subspecs/forkchoice/store.py | 44 +++--- .../test_attestation_aggregation.py | 29 ++-- .../forkchoice/test_store_attestations.py | 7 +- tests/lean_spec/subspecs/ssz/test_block.py | 4 +- 15 files changed, 278 insertions(+), 231 deletions(-) diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index f8d15a3e..fb4ce375 100644 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -30,6 +30,11 @@ from typing import TYPE_CHECKING, Iterator, Self from lean_spec.subspecs.containers import AttestationData +from lean_spec.subspecs.containers.block.types import ( + AggregatedAttestations, + AttestationSignatures, + NaiveAggregatedSignatures, +) from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.containers import PublicKey, SecretKey, Signature @@ -208,6 +213,59 @@ def sign_attestation_data( message = bytes(hash_tree_root(attestation_data)) return self.scheme.sign(sk, epoch, message) + def build_attestation_signatures( + self, + aggregated_attestations: AggregatedAttestations, + signature_lookup: "Mapping[tuple[int, bytes], Signature] | None" = None, + ) -> AttestationSignatures: + """ + Build `AttestationSignatures` for already-aggregated attestations. + + This is a convenience helper for tests/fixtures that need to produce + `BlockSignatures.attestation_signatures` for a block. + + Args: + aggregated_attestations: Iterable of aggregated attestation containers. + Each item is expected to have: + - `.data` (AttestationData) + - `.aggregation_bits.to_validator_indices()` (Iterable[Uint64]) + signature_lookup: Optional override map keyed by + `(int(validator_id), bytes(hash_tree_root(attestation_data))) -> signature`. + When provided and a key exists, that signature is used instead of signing. + + Returns: + AttestationSignatures matching the ordering of `aggregated_attestations` + and per-attestation validator index ordering. + """ + attestation_data_roots: dict[int, bytes] = {} + + def _data_root_bytes(attestation_data: AttestationData) -> bytes: + key = id(attestation_data) + if key not in attestation_data_roots: + attestation_data_roots[key] = bytes(hash_tree_root(attestation_data)) + return attestation_data_roots[key] + + return AttestationSignatures( + data=[ + NaiveAggregatedSignatures( + data=[ + ( + signature_lookup.get( + (int(validator_id), _data_root_bytes(aggregated_attestation.data)) + ) + if signature_lookup is not None + else None + ) + or self.sign_attestation_data(validator_id, aggregated_attestation.data) + for validator_id in ( + aggregated_attestation.aggregation_bits.to_validator_indices() + ) + ] + ) + for aggregated_attestation in aggregated_attestations + ] + ) + def _generate_single_keypair(num_epochs: int) -> dict[str, str]: """Generate one key pair (module-level for pickling in ProcessPoolExecutor).""" 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 21fa9e47..bef3295e 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py +++ b/packages/testing/src/consensus_testing/test_fixtures/fork_choice.py @@ -22,7 +22,6 @@ ) from lean_spec.subspecs.containers.block.types import ( AggregatedAttestations, - NaiveAggregatedSignature, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot @@ -348,9 +347,10 @@ def _build_block_from_spec( ) # Sign all attestations and the proposer attestation - attestation_signatures = [ - key_manager.sign_attestation_data(att.validator_id, att.data) for att in attestations - ] + attestation_signatures = key_manager.build_attestation_signatures( + final_block.body.attestations + ) + proposer_signature = key_manager.sign_attestation_data( proposer_attestation.validator_id, proposer_attestation.data, @@ -362,7 +362,7 @@ def _build_block_from_spec( proposer_attestation=proposer_attestation, ), signature=BlockSignatures( - attestation_signatures=NaiveAggregatedSignature(data=attestation_signatures), + attestation_signatures=attestation_signatures, proposer_signature=proposer_signature, ), ) 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 afc58115..5a4c23b3 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -4,10 +4,6 @@ from pydantic import ConfigDict, PrivateAttr, field_serializer -from lean_spec.subspecs.containers.attestation import ( - aggregated_attestations_to_plain, - attestation_to_aggregated, -) from lean_spec.subspecs.containers.block.block import Block, BlockBody from lean_spec.subspecs.containers.block.types import AggregatedAttestations from lean_spec.subspecs.containers.state.state import State @@ -211,11 +207,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, # Extract attestations from body if provided, converting from aggregated form # Flatten all plain attestations from all aggregated attestations attestations = ( - [ - plain_att - for att in spec.body.attestations - for plain_att in aggregated_attestations_to_plain(att) - ] + [plain_att for att in spec.body.attestations for plain_att in att.to_plain()] if spec.body else [] ) @@ -241,7 +233,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, body=spec.body or BlockBody( attestations=AggregatedAttestations( - data=[attestation_to_aggregated(att) for att in attestations] + data=[att.to_aggregated() for att in attestations] ) ), ) 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 6517a458..70db4933 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -17,7 +17,6 @@ BlockWithAttestation, SignedBlockWithAttestation, ) -from lean_spec.subspecs.containers.block.types import NaiveAggregatedSignature from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.containers.state.state import State @@ -193,7 +192,9 @@ def _build_block_from_spec( parent_root = hash_tree_root(parent_state.latest_block_header) # Build attestations from spec - attestations, signatures = self._build_attestations_from_spec(spec, state, key_manager) + attestations, attestation_signature_inputs = self._build_attestations_from_spec( + spec, state, key_manager + ) # Use State.build_block for core block building (pure spec logic) final_block, _, _, _ = state.build_block( @@ -203,6 +204,21 @@ def _build_block_from_spec( attestations=attestations, ) + # Preserve per-attestation validity from the spec. + # + # `State.build_block()` aggregates attestations by data into + # `final_block.body.attestations`. For signature tests we must ensure that + # any intentionally-invalid signature from the input spec remains invalid + # in the produced `SignedBlockWithAttestation`. + signature_lookup: dict[tuple[int, bytes], Any] = { + (int(att.validator_id), bytes(hash_tree_root(att.data))): sig + for att, sig in zip(attestations, attestation_signature_inputs, strict=True) + } + attestation_signatures = key_manager.build_attestation_signatures( + final_block.body.attestations, + signature_lookup=signature_lookup, + ) + # Create proposer attestation for this block block_root = hash_tree_root(final_block) proposer_attestation = Attestation( @@ -239,7 +255,7 @@ def _build_block_from_spec( proposer_attestation=proposer_attestation, ), signature=BlockSignatures( - attestation_signatures=NaiveAggregatedSignature(data=signatures), + attestation_signatures=attestation_signatures, proposer_signature=proposer_attestation_signature, ), ) diff --git a/src/lean_spec/subspecs/containers/attestation/__init__.py b/src/lean_spec/subspecs/containers/attestation/__init__.py index 7cf74651..5e82ce4b 100644 --- a/src/lean_spec/subspecs/containers/attestation/__init__.py +++ b/src/lean_spec/subspecs/containers/attestation/__init__.py @@ -6,10 +6,6 @@ AttestationData, SignedAggregatedAttestations, SignedAttestation, - aggregate_attestations_by_data, - aggregated_attestations_to_plain, - aggregation_bits_to_validator_indices, - attestation_to_aggregated, ) from .types import AggregatedSignatures, AggregationBits @@ -21,8 +17,4 @@ "AggregatedAttestation", "AggregatedSignatures", "AggregationBits", - "aggregate_attestations_by_data", - "aggregation_bits_to_validator_indices", - "aggregated_attestations_to_plain", - "attestation_to_aggregated", ] diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index 565ce506..478cab8c 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -12,6 +12,8 @@ doesn't do this yet. """ +from __future__ import annotations + from collections import defaultdict from lean_spec.subspecs.containers.slot import Slot @@ -47,6 +49,16 @@ class Attestation(Container): data: AttestationData """The attestation data produced by the validator.""" + def to_aggregated(self) -> AggregatedAttestation: + """Convert this plain Attestation into the aggregated representation.""" + validator_index = int(self.validator_id) + bits = [False] * (validator_index + 1) + bits[validator_index] = True + return AggregatedAttestation( + aggregation_bits=AggregationBits(data=bits), + data=self.data, + ) + class SignedAttestation(Container): """Validator attestation bundled with its signature.""" @@ -74,6 +86,47 @@ class AggregatedAttestation(Container): committee assignments. """ + def to_plain(self) -> list[Attestation]: + """ + Expand this aggregated attestation into plain per-validator attestations. + + Returns: + One `Attestation` per participating validator index, all sharing the same + `AttestationData`. + """ + validator_indices = self.aggregation_bits.to_validator_indices() + return [ + Attestation(validator_id=validator_id, data=self.data) + for validator_id in validator_indices + ] + + @classmethod + def aggregate_by_data( + cls, + attestations: list[Attestation], + ) -> list[AggregatedAttestation]: + """ + Aggregate plain per-validator attestations by their shared AttestationData. + + Args: + attestations: Attestations to aggregate. + + Returns: + One AggregatedAttestation per unique AttestationData, with aggregation + bits set for all participating validators. + """ + data_to_validator_ids: dict[AttestationData, list[Uint64]] = defaultdict(list) + for attestation in attestations: + data_to_validator_ids[attestation.data].append(attestation.validator_id) + + return [ + cls( + aggregation_bits=AggregationBits.from_validator_indices(validator_ids), + data=data, + ) + for data, validator_ids in data_to_validator_ids.items() + ] + class SignedAggregatedAttestations(Container): """Aggregated attestation bundled with aggregated signatures.""" @@ -92,98 +145,3 @@ class SignedAggregatedAttestations(Container): - this will be replaced by a SNARK in future devnets. - this will be aggregated by aggregators in future devnets. """ - - -def aggregation_bits_to_validator_indices(bits: AggregationBits) -> list[Uint64]: - """ - Extract all validator indices encoded in aggregation bits. - - Returns the list of all validators who participated in the aggregation, - sorted by validator index. - - Args: - bits: Aggregation bitlist with participating validators. - - Returns: - List of validator indices, sorted in ascending order. - """ - validator_indices = [Uint64(index) for index, bit in enumerate(bits) if bool(bit)] - if not validator_indices: - raise AssertionError("Aggregated attestation must reference at least one validator") - return validator_indices - - -def aggregated_attestations_to_plain( - aggregated: AggregatedAttestation, -) -> list[Attestation]: - """ - Convert aggregated attestation to a list of plain Attestation containers. - - Extracts all participating validator indices from the aggregation bits - and creates individual Attestation objects for each validator. - - Args: - aggregated: Aggregated attestation with one or more participating validators. - - Returns: - List of plain attestations, one per participating validator. - """ - validator_indices = aggregation_bits_to_validator_indices(aggregated.aggregation_bits) - return [ - Attestation(validator_id=validator_id, data=aggregated.data) - for validator_id in validator_indices - ] - - -def attestation_to_aggregated(attestation: Attestation) -> AggregatedAttestation: - """Convert a plain Attestation into the aggregated representation.""" - validator_index = int(attestation.validator_id) - bits = [False] * (validator_index + 1) - bits[validator_index] = True - return AggregatedAttestation( - aggregation_bits=AggregationBits(data=bits), - data=attestation.data, - ) - - -def aggregate_attestations_by_data( - attestations: list[Attestation], -) -> list[AggregatedAttestation]: - """ - Aggregate attestations with common attestation data. - - Groups attestations by their AttestationData and creates one AggregatedAttestation - per unique data, with all participating validator bits set. - - Args: - attestations: List of attestations to aggregate. - - Returns: - List of aggregated attestations with proper bit aggregation. - """ - # Group validator IDs by attestation data (avoids intermediate objects) - data_to_validator_ids: dict[AttestationData, list[int]] = defaultdict(list) - - for attestation in attestations: - data_to_validator_ids[attestation.data].append(int(attestation.validator_id)) - - # Create aggregated attestations with all relevant bits set - result: list[AggregatedAttestation] = [] - - for data, validator_ids in data_to_validator_ids.items(): - # Find the maximum validator index to determine bitlist size - max_validator_id = max(validator_ids) - - # Create bitlist with all participating validators set to True - bits = [False] * (max_validator_id + 1) - for validator_id in validator_ids: - bits[validator_id] = True - - result.append( - AggregatedAttestation( - aggregation_bits=AggregationBits(data=bits), - data=data, - ) - ) - - return result diff --git a/src/lean_spec/subspecs/containers/attestation/types.py b/src/lean_spec/subspecs/containers/attestation/types.py index c4a7b99a..0370e589 100644 --- a/src/lean_spec/subspecs/containers/attestation/types.py +++ b/src/lean_spec/subspecs/containers/attestation/types.py @@ -1,6 +1,10 @@ """Attestation-related SSZ types for the Lean consensus specification.""" -from lean_spec.types import SSZList +from __future__ import annotations + +from collections.abc import Iterable + +from lean_spec.types import SSZList, Uint64 from lean_spec.types.bitfields import BaseBitlist from ...chain.config import VALIDATOR_REGISTRY_LIMIT @@ -12,6 +16,50 @@ class AggregationBits(BaseBitlist): LIMIT = int(VALIDATOR_REGISTRY_LIMIT) + @classmethod + def from_validator_indices(cls, indices: Iterable[Uint64 | int]) -> "AggregationBits": + """ + Construct aggregation bits from a set of validator indices. + + Args: + indices: Validator indices to set in the bitlist. + + Returns: + AggregationBits with the corresponding indices set to True. + + Raises: + AssertionError: If no indices are provided. + AssertionError: If any index is outside the supported LIMIT. + """ + ids = [int(i) for i in indices] + if not ids: + raise AssertionError("Aggregated attestation must reference at least one validator") + + max_id = max(ids) + if max_id >= cls.LIMIT: + raise AssertionError("Validator index out of range for aggregation bits") + + bits = [False] * (max_id + 1) + for i in ids: + bits[i] = True + + return cls(data=bits) + + def to_validator_indices(self) -> list[Uint64]: + """ + Extract all validator indices encoded in these aggregation bits. + + Returns: + List of validator indices, sorted in ascending order. + + Raises: + AssertionError: If no bits are set. + """ + if not (indices := [Uint64(i) for i, bit in enumerate(self.data) if bool(bit)]): + raise AssertionError("Aggregated attestation must reference at least one validator") + + return indices + class AggregatedSignatures(SSZList): """Naive list of validator signatures used for aggregation placeholders.""" diff --git a/src/lean_spec/subspecs/containers/block/__init__.py b/src/lean_spec/subspecs/containers/block/__init__.py index ffadc676..819745d6 100644 --- a/src/lean_spec/subspecs/containers/block/__init__.py +++ b/src/lean_spec/subspecs/containers/block/__init__.py @@ -8,7 +8,11 @@ BlockWithAttestation, SignedBlockWithAttestation, ) -from .types import AggregatedAttestations, NaiveAggregatedSignature +from .types import ( + AggregatedAttestations, + AttestationSignatures, + NaiveAggregatedSignatures, +) __all__ = [ "Block", @@ -18,5 +22,6 @@ "BlockWithAttestation", "SignedBlockWithAttestation", "AggregatedAttestations", - "NaiveAggregatedSignature", + "NaiveAggregatedSignatures", + "AttestationSignatures", ] diff --git a/src/lean_spec/subspecs/containers/block/block.py b/src/lean_spec/subspecs/containers/block/block.py index 92103460..d4f99e1a 100644 --- a/src/lean_spec/subspecs/containers/block/block.py +++ b/src/lean_spec/subspecs/containers/block/block.py @@ -9,7 +9,6 @@ can propose. """ -from itertools import pairwise from typing import TYPE_CHECKING, cast from lean_spec.subspecs.containers.slot import Slot @@ -18,9 +17,12 @@ from lean_spec.types.container import Container from ...xmss.containers import Signature as XmssSignature -from ..attestation import Attestation, AttestationData, aggregation_bits_to_validator_indices +from ..attestation import Attestation from ..validator import Validator -from .types import AggregatedAttestations, NaiveAggregatedSignature +from .types import ( + AggregatedAttestations, + AttestationSignatures, +) if TYPE_CHECKING: from ..state import State @@ -102,13 +104,15 @@ class BlockWithAttestation(Container): class BlockSignatures(Container): """Signature payload for the block.""" - attestation_signatures: NaiveAggregatedSignature - """Signatures for the attestations in the block body. + attestation_signatures: AttestationSignatures + """Attestation signatures for the aggregated attestations in the block body. - Contains a naive list of signatures for the attestations in the block body. + Each entry corresponds to an aggregated attestation from the block body and + contains all XMSS signatures from the participating validators. TODO: - - this will be replaced by a BytesArray in next PR to include leanVM aggregated sproof. + - Currently, this is list of lists of signatures. + - The list of signatures will be replaced by a BytesArray to include leanVM aggregated proof. """ proposer_signature: XmssSignature @@ -157,50 +161,37 @@ def verify_signatures(self, parent_state: "State") -> bool: """ block = self.message.block signatures = self.signature - block_attestations = block.body.attestations + aggregated_attestations = block.body.attestations attestation_signatures = signatures.attestation_signatures + assert len(aggregated_attestations) == len(attestation_signatures), ( + "Attestation signature groups must align with block body attestations" + ) + validators = parent_state.validators - # Collect all validator IDs and their corresponding attestation data - # from the aggregated attestations - validator_attestations: list[tuple[Uint64, AttestationData]] = [] + for aggregated_attestation, aggregated_signature in zip( + aggregated_attestations, attestation_signatures, strict=True + ): + validator_ids = aggregated_attestation.aggregation_bits.to_validator_indices() - for aggregated_attestation in block_attestations: - validator_ids = aggregation_bits_to_validator_indices( - aggregated_attestation.aggregation_bits + assert len(aggregated_signature) == len(validator_ids), ( + "Aggregated attestation signature count mismatch" ) - for validator_id in validator_ids: - validator_attestations.append((validator_id, aggregated_attestation.data)) - # Sort by validator_id to match the sorted signature order - validator_attestations.sort(key=lambda x: x[0]) + attestation_root = bytes(hash_tree_root(aggregated_attestation.data)) - # Verify signature count matches total validator count - assert len(attestation_signatures) == len(validator_attestations), ( - "Number of attestation signatures does not match validator count" - ) + # Verify each validator's attestation signature + for validator_id, signature in zip(validator_ids, aggregated_signature, strict=True): + # Ensure validator exists in the active set + assert validator_id < Uint64(len(validators)), "Validator index out of range" + validator = cast(Validator, validators[validator_id]) - # Verify signatures are in strictly ascending order by validator_id - # This ensures the block builder properly sorted the signatures - assert all(prev[0] < curr[0] for prev, curr in pairwise(validator_attestations)), ( - "Attestation signatures must be in strictly ascending order by validator_id. " - f"Found: {[vid for vid, _ in validator_attestations]}" - ) - - # Verify each validator's attestation signature - for (validator_id, attestation_data), signature in zip( - validator_attestations, attestation_signatures, strict=True - ): - # Ensure validator exists in the active set - assert validator_id < Uint64(len(validators)), "Validator index out of range" - validator = cast(Validator, validators[validator_id]) - - assert signature.verify( - validator.get_pubkey(), - attestation_data.slot, - bytes(hash_tree_root(attestation_data)), - ), "Attestation signature verification failed" + assert signature.verify( + validator.get_pubkey(), + aggregated_attestation.data.slot, + attestation_root, + ), "Attestation signature verification failed" # Verify proposer attestation signature proposer_attestation = self.message.proposer_attestation diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index 2fde6c10..c6d62c38 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -14,8 +14,15 @@ class AggregatedAttestations(SSZList): LIMIT = int(VALIDATOR_REGISTRY_LIMIT) -class NaiveAggregatedSignature(SSZList): +class NaiveAggregatedSignatures(SSZList): """Aggregated signature list included alongside the block proposer's attestation.""" ELEMENT_TYPE = XmssSignature LIMIT = int(VALIDATOR_REGISTRY_LIMIT) + + +class AttestationSignatures(SSZList): + """List of per-attestation naive signature lists aligned with block body attestations.""" + + ELEMENT_TYPE = NaiveAggregatedSignatures + LIMIT = int(VALIDATOR_REGISTRY_LIMIT) diff --git a/src/lean_spec/subspecs/containers/state/state.py b/src/lean_spec/subspecs/containers/state/state.py index cf659ba5..430d0019 100644 --- a/src/lean_spec/subspecs/containers/state/state.py +++ b/src/lean_spec/subspecs/containers/state/state.py @@ -13,10 +13,9 @@ ) from ..attestation import ( + AggregatedAttestation, Attestation, SignedAttestation, - aggregate_attestations_by_data, - aggregated_attestations_to_plain, ) if TYPE_CHECKING: @@ -375,13 +374,13 @@ def process_block(self, block: Block) -> "State": raise AssertionError("Block contains duplicate AttestationData") attestations_data.add(aggregated_att.data) - attestations.extend(aggregated_attestations_to_plain(aggregated_att)) + attestations.extend(aggregated_att.to_plain()) return state.process_attestations(attestations) def process_attestations( self, - attestations: Iterable[Attestation], + attestations: list[Attestation], ) -> "State": """ Apply attestations and update justification/finalization @@ -671,7 +670,7 @@ def build_block( state_root=Bytes32.zero(), body=BlockBody( attestations=AggregatedAttestations( - data=aggregate_attestations_by_data(attestations) + data=AggregatedAttestation.aggregate_by_data(attestations) ) ), ) @@ -715,20 +714,7 @@ def build_block( attestations.extend(new_attestations) signatures.extend(new_signatures) - # Sort attestations and signatures by validator_id to ensure - # consistent ordering for signature validation. - # Only sort if we have signatures (i.e., attestation collection was performed) - if signatures: - attestations_with_sigs = sorted( - zip(attestations, signatures, strict=True), key=lambda x: x[0].validator_id - ) - sorted_attestations = [att for att, _ in attestations_with_sigs] - sorted_signatures = [sig for _, sig in attestations_with_sigs] - else: - sorted_attestations = attestations - sorted_signatures = signatures - # Store the post state root in the block final_block = candidate_block.model_copy(update={"state_root": hash_tree_root(post_state)}) - return final_block, post_state, sorted_attestations, sorted_signatures + return final_block, post_state, attestations, signatures diff --git a/src/lean_spec/subspecs/forkchoice/store.py b/src/lean_spec/subspecs/forkchoice/store.py index 9bc058e2..f6bba9db 100644 --- a/src/lean_spec/subspecs/forkchoice/store.py +++ b/src/lean_spec/subspecs/forkchoice/store.py @@ -30,7 +30,6 @@ SignedBlockWithAttestation, State, ) -from lean_spec.subspecs.containers.attestation import aggregated_attestations_to_plain from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.containers import Signature @@ -458,33 +457,34 @@ def on_block(self, signed_block_with_attestation: SignedBlockWithAttestation) -> ) # Process block body attestations. - block_attestations = signed_block_with_attestation.message.block.body.attestations + aggregated_attestations = signed_block_with_attestation.message.block.body.attestations attestation_signatures = signed_block_with_attestation.signature.attestation_signatures - # Expand aggregated attestations into individual validator attestations - plain_attestations = [ - plain_att - for aggregated_att in block_attestations - for plain_att in aggregated_attestations_to_plain(aggregated_att) - ] - - # Sort attestations by validator_id to align with signature order - plain_attestations.sort(key=lambda att: att.validator_id) - - assert len(plain_attestations) == len(attestation_signatures), ( - "Attestation signature list must align with validator attestations" + assert len(aggregated_attestations) == len(attestation_signatures), ( + "Attestation signature groups must match aggregated attestations" ) - for attestation, signature in zip(plain_attestations, attestation_signatures, strict=True): - store = store.on_attestation( - signed_attestation=SignedAttestation( - validator_id=attestation.validator_id, - message=attestation.data, - signature=signature, - ), - is_from_block=True, + for aggregated_attestation, aggregated_signature in zip( + aggregated_attestations, attestation_signatures, strict=True + ): + plain_attestations = aggregated_attestation.to_plain() + + assert len(plain_attestations) == len(aggregated_signature), ( + "Aggregated attestation signature count mismatch" ) + for attestation, signature in zip( + plain_attestations, aggregated_signature, strict=True + ): + store = store.on_attestation( + signed_attestation=SignedAttestation( + validator_id=attestation.validator_id, + message=attestation.data, + signature=signature, + ), + is_from_block=True, + ) + # Update forkchoice head based on new block and attestations # # IMPORTANT: This must happen BEFORE processing proposer attestation diff --git a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py index 369939dc..1d5e2e13 100644 --- a/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py +++ b/tests/lean_spec/subspecs/containers/test_attestation_aggregation.py @@ -3,11 +3,10 @@ import pytest from lean_spec.subspecs.containers.attestation import ( + AggregatedAttestation, AggregationBits, Attestation, AttestationData, - aggregate_attestations_by_data, - aggregation_bits_to_validator_indices, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot @@ -21,7 +20,7 @@ def test_reject_empty_aggregation_bits(self) -> None: """Validate aggregated attestation must include at least one validator.""" bits = AggregationBits(data=[False, False, False]) with pytest.raises(AssertionError, match="at least one validator"): - aggregation_bits_to_validator_indices(bits) + bits.to_validator_indices() def test_aggregate_attestations_by_common_data(self) -> None: """Test that attestations with same data are properly aggregated.""" @@ -45,21 +44,21 @@ def test_aggregate_attestations_by_common_data(self) -> None: Attestation(validator_id=Uint64(5), data=att_data2), ] - aggregated = aggregate_attestations_by_data(attestations) + aggregated = AggregatedAttestation.aggregate_by_data(attestations) # Should have 2 aggregated attestations (one per unique data) assert len(aggregated) == 2 # Find the aggregated attestation with att_data1 agg1 = next(agg for agg in aggregated if agg.data == att_data1) - validator_ids1 = aggregation_bits_to_validator_indices(agg1.aggregation_bits) + validator_ids1 = agg1.aggregation_bits.to_validator_indices() # Should contain validators 1 and 3 assert set(validator_ids1) == {Uint64(1), Uint64(3)} # Find the aggregated attestation with att_data2 agg2 = next(agg for agg in aggregated if agg.data == att_data2) - validator_ids2 = aggregation_bits_to_validator_indices(agg2.aggregation_bits) + validator_ids2 = agg2.aggregation_bits.to_validator_indices() # Should contain only validator 5 assert set(validator_ids2) == {Uint64(5)} @@ -79,10 +78,10 @@ def test_aggregate_attestations_sets_all_bits(self) -> None: Attestation(validator_id=Uint64(10), data=att_data), ] - aggregated = aggregate_attestations_by_data(attestations) + aggregated = AggregatedAttestation.aggregate_by_data(attestations) assert len(aggregated) == 1 - validator_ids = aggregation_bits_to_validator_indices(aggregated[0].aggregation_bits) + validator_ids = aggregated[0].aggregation_bits.to_validator_indices() # Should have all three validators assert len(validator_ids) == 3 @@ -90,7 +89,7 @@ def test_aggregate_attestations_sets_all_bits(self) -> None: def test_aggregate_empty_attestations(self) -> None: """Test aggregation with no attestations.""" - aggregated = aggregate_attestations_by_data([]) + aggregated = AggregatedAttestation.aggregate_by_data([]) assert len(aggregated) == 0 def test_aggregate_single_attestation(self) -> None: @@ -104,10 +103,10 @@ def test_aggregate_single_attestation(self) -> None: attestations = [Attestation(validator_id=Uint64(5), data=att_data)] - aggregated = aggregate_attestations_by_data(attestations) + aggregated = AggregatedAttestation.aggregate_by_data(attestations) assert len(aggregated) == 1 - validator_ids = aggregation_bits_to_validator_indices(aggregated[0].aggregation_bits) + validator_ids = aggregated[0].aggregation_bits.to_validator_indices() assert validator_ids == [Uint64(5)] @@ -135,13 +134,7 @@ def test_duplicate_attestation_data_detection(self) -> None: data=att_data, ) - from lean_spec.subspecs.containers.attestation import aggregated_attestations_to_plain - - plain = [ - plain_att - for aggregated in (agg1, agg2) - for plain_att in aggregated_attestations_to_plain(aggregated) - ] + plain = [plain_att for aggregated in (agg1, agg2) for plain_att in aggregated.to_plain()] # Expect 2 plain attestations (because validator 1 is common in agg1 and agg2) # validator 1 and validator 2 are the only unique validators in the attestations diff --git a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py index b1c9e1a8..c651db07 100644 --- a/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py +++ b/tests/lean_spec/subspecs/forkchoice/test_store_attestations.py @@ -17,7 +17,6 @@ ) from lean_spec.subspecs.containers.block.types import ( AggregatedAttestations, - NaiveAggregatedSignature, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.containers.slot import Slot @@ -66,7 +65,7 @@ def test_on_block_processes_multi_validator_aggregations() -> None: # For slot 1 with 3 validators: 1 % 3 == 1, so validator 1 is the proposer proposer_index = Uint64(1) - _, block, attestation_signatures = producer_store.produce_block_with_signatures( + _, block, _ = producer_store.produce_block_with_signatures( attestation_slot, proposer_index, ) @@ -87,13 +86,15 @@ def test_on_block_processes_multi_validator_aggregations() -> None: proposer_attestation.data, ) + attestation_signatures = key_manager.build_attestation_signatures(block.body.attestations) + signed_block = SignedBlockWithAttestation( message=BlockWithAttestation( block=block, proposer_attestation=proposer_attestation, ), signature=BlockSignatures( - attestation_signatures=NaiveAggregatedSignature(data=attestation_signatures), + attestation_signatures=attestation_signatures, proposer_signature=proposer_signature, ), ) diff --git a/tests/lean_spec/subspecs/ssz/test_block.py b/tests/lean_spec/subspecs/ssz/test_block.py index 6423bf36..a7f880db 100644 --- a/tests/lean_spec/subspecs/ssz/test_block.py +++ b/tests/lean_spec/subspecs/ssz/test_block.py @@ -8,7 +8,7 @@ ) from lean_spec.subspecs.containers.block.types import ( AggregatedAttestations, - NaiveAggregatedSignature, + AttestationSignatures, ) from lean_spec.subspecs.containers.checkpoint import Checkpoint from lean_spec.subspecs.koalabear import Fp @@ -39,7 +39,7 @@ def test_encode_decode_signed_block_with_attestation_roundtrip() -> None: ), ), signature=BlockSignatures( - attestation_signatures=NaiveAggregatedSignature(data=[]), + attestation_signatures=AttestationSignatures(data=[]), proposer_signature=Signature( path=HashTreeOpening(siblings=HashDigestList(data=[])), rho=Randomness(data=[Fp(0) for _ in range(PROD_CONFIG.RAND_LEN_FE)]), From a9bd016f2eb445043664d09fc3f8d69d82e24bce Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Sat, 13 Dec 2025 16:31:25 +0530 Subject: [PATCH 08/12] fix: make it more pythonic --- .../testing/src/consensus_testing/keys.py | 14 +++-------- .../test_fixtures/state_transition.py | 24 ++++++++----------- .../test_fixtures/verify_signatures.py | 9 ++++--- .../containers/attestation/attestation.py | 5 ++++ .../subspecs/containers/attestation/types.py | 4 +--- .../subspecs/containers/state/state.py | 4 ++-- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index fb4ce375..e505b741 100644 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -216,7 +216,7 @@ def sign_attestation_data( def build_attestation_signatures( self, aggregated_attestations: AggregatedAttestations, - signature_lookup: "Mapping[tuple[int, bytes], Signature] | None" = None, + signature_lookup: Mapping[tuple[Uint64, bytes], Signature] | None = None, ) -> AttestationSignatures: """ Build `AttestationSignatures` for already-aggregated attestations. @@ -230,28 +230,20 @@ def build_attestation_signatures( - `.data` (AttestationData) - `.aggregation_bits.to_validator_indices()` (Iterable[Uint64]) signature_lookup: Optional override map keyed by - `(int(validator_id), bytes(hash_tree_root(attestation_data))) -> signature`. + `(validator_id, bytes(hash_tree_root(attestation_data))) -> signature`. When provided and a key exists, that signature is used instead of signing. Returns: AttestationSignatures matching the ordering of `aggregated_attestations` and per-attestation validator index ordering. """ - attestation_data_roots: dict[int, bytes] = {} - - def _data_root_bytes(attestation_data: AttestationData) -> bytes: - key = id(attestation_data) - if key not in attestation_data_roots: - attestation_data_roots[key] = bytes(hash_tree_root(attestation_data)) - return attestation_data_roots[key] - return AttestationSignatures( data=[ NaiveAggregatedSignatures( data=[ ( signature_lookup.get( - (int(validator_id), _data_root_bytes(aggregated_attestation.data)) + (validator_id, aggregated_attestation.data.data_root_bytes()) ) if signature_lookup is not None else None 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 5a4c23b3..ca4985d0 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -204,12 +204,9 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, temp_state = state.process_slots(spec.slot) parent_root = hash_tree_root(temp_state.latest_block_header) - # Extract attestations from body if provided, converting from aggregated form - # Flatten all plain attestations from all aggregated attestations - attestations = ( - [plain_att for att in spec.body.attestations for plain_att in att.to_plain()] - if spec.body - else [] + # Extract attestations from body if provided + aggregated_attestations = ( + spec.body.attestations if spec.body else AggregatedAttestations(data=[]) ) # Handle explicit state root override @@ -219,7 +216,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, proposer_index=proposer_index, parent_root=parent_root, state_root=spec.state_root, - body=spec.body or BlockBody(attestations=AggregatedAttestations(data=[])), + body=spec.body or BlockBody(attestations=aggregated_attestations), ) return block, None @@ -230,12 +227,7 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, proposer_index=proposer_index, parent_root=parent_root, state_root=Bytes32.zero(), - body=spec.body - or BlockBody( - attestations=AggregatedAttestations( - data=[att.to_aggregated() for att in attestations] - ) - ), + body=spec.body or BlockBody(attestations=aggregated_attestations), ) return block, None @@ -244,6 +236,10 @@ def _build_block_from_spec(self, spec: BlockSpec, state: State) -> tuple[Block, slot=spec.slot, proposer_index=proposer_index, parent_root=parent_root, - attestations=attestations, + attestations=[ + attestation + for aggregated_attestation in aggregated_attestations + for attestation in aggregated_attestation.to_plain() + ], ) return block, post_state 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 70db4933..eaf0e1b4 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -206,12 +206,11 @@ def _build_block_from_spec( # Preserve per-attestation validity from the spec. # - # `State.build_block()` aggregates attestations by data into - # `final_block.body.attestations`. For signature tests we must ensure that - # any intentionally-invalid signature from the input spec remains invalid + # For signature tests we must ensure that the signatures in the input spec are used + # for any intentionally-invalid signature from the input spec remains invalid # in the produced `SignedBlockWithAttestation`. - signature_lookup: dict[tuple[int, bytes], Any] = { - (int(att.validator_id), bytes(hash_tree_root(att.data))): sig + signature_lookup: dict[tuple[Uint64, bytes], Signature] = { + (att.validator_id, bytes(hash_tree_root(att.data))): sig for att, sig in zip(attestations, attestation_signature_inputs, strict=True) } attestation_signatures = key_manager.build_attestation_signatures( diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index 478cab8c..5fb19afb 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -17,6 +17,7 @@ from collections import defaultdict from lean_spec.subspecs.containers.slot import Slot +from lean_spec.subspecs.ssz import hash_tree_root from lean_spec.types import Container, Uint64 from ...xmss.containers import Signature @@ -39,6 +40,10 @@ class AttestationData(Container): source: Checkpoint """The checkpoint representing the source block as observed by the validator.""" + def data_root_bytes(self) -> bytes: + """The root of the attestation data.""" + return bytes(hash_tree_root(self)) + class Attestation(Container): """Validator specific attestation wrapping shared attestation data.""" diff --git a/src/lean_spec/subspecs/containers/attestation/types.py b/src/lean_spec/subspecs/containers/attestation/types.py index 0370e589..904a27be 100644 --- a/src/lean_spec/subspecs/containers/attestation/types.py +++ b/src/lean_spec/subspecs/containers/attestation/types.py @@ -2,8 +2,6 @@ from __future__ import annotations -from collections.abc import Iterable - from lean_spec.types import SSZList, Uint64 from lean_spec.types.bitfields import BaseBitlist @@ -17,7 +15,7 @@ class AggregationBits(BaseBitlist): LIMIT = int(VALIDATOR_REGISTRY_LIMIT) @classmethod - def from_validator_indices(cls, indices: Iterable[Uint64 | int]) -> "AggregationBits": + def from_validator_indices(cls, indices: list[Uint64]) -> AggregationBits: """ Construct aggregation bits from a set of validator indices. diff --git a/src/lean_spec/subspecs/containers/state/state.py b/src/lean_spec/subspecs/containers/state/state.py index 430d0019..f6a9476f 100644 --- a/src/lean_spec/subspecs/containers/state/state.py +++ b/src/lean_spec/subspecs/containers/state/state.py @@ -393,8 +393,8 @@ def process_attestations( Parameters ---------- - attestations : Iterable[Attestation] - The attestations to process. + attestations : Attestations + The list of attestations to process. Returns: ------- From 3338bbb94c252a70069492d158c1ddf47bfd46a9 Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Sat, 13 Dec 2025 18:24:31 +0530 Subject: [PATCH 09/12] fix: address comments --- src/lean_spec/subspecs/containers/__init__.py | 4 ++-- .../subspecs/containers/attestation/__init__.py | 4 ++-- .../subspecs/containers/attestation/attestation.py | 12 +----------- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/lean_spec/subspecs/containers/__init__.py b/src/lean_spec/subspecs/containers/__init__.py index 0ac92048..d04aa685 100644 --- a/src/lean_spec/subspecs/containers/__init__.py +++ b/src/lean_spec/subspecs/containers/__init__.py @@ -14,7 +14,7 @@ AggregationBits, Attestation, AttestationData, - SignedAggregatedAttestations, + SignedAggregatedAttestation, SignedAttestation, ) from .block import ( @@ -36,7 +36,7 @@ "AttestationData", "Attestation", "SignedAttestation", - "SignedAggregatedAttestations", + "SignedAggregatedAttestation", "Block", "BlockWithAttestation", "BlockBody", diff --git a/src/lean_spec/subspecs/containers/attestation/__init__.py b/src/lean_spec/subspecs/containers/attestation/__init__.py index 5e82ce4b..70ea5ba8 100644 --- a/src/lean_spec/subspecs/containers/attestation/__init__.py +++ b/src/lean_spec/subspecs/containers/attestation/__init__.py @@ -4,7 +4,7 @@ AggregatedAttestation, Attestation, AttestationData, - SignedAggregatedAttestations, + SignedAggregatedAttestation, SignedAttestation, ) from .types import AggregatedSignatures, AggregationBits @@ -13,7 +13,7 @@ "AttestationData", "Attestation", "SignedAttestation", - "SignedAggregatedAttestations", + "SignedAggregatedAttestation", "AggregatedAttestation", "AggregatedSignatures", "AggregationBits", diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index 5fb19afb..e64d4588 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -54,16 +54,6 @@ class Attestation(Container): data: AttestationData """The attestation data produced by the validator.""" - def to_aggregated(self) -> AggregatedAttestation: - """Convert this plain Attestation into the aggregated representation.""" - validator_index = int(self.validator_id) - bits = [False] * (validator_index + 1) - bits[validator_index] = True - return AggregatedAttestation( - aggregation_bits=AggregationBits(data=bits), - data=self.data, - ) - class SignedAttestation(Container): """Validator attestation bundled with its signature.""" @@ -133,7 +123,7 @@ def aggregate_by_data( ] -class SignedAggregatedAttestations(Container): +class SignedAggregatedAttestation(Container): """Aggregated attestation bundled with aggregated signatures.""" message: AggregatedAttestation From 7e7809401543649cf46c20f4b4741c46bff2f9cb Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Sat, 13 Dec 2025 23:15:59 +0530 Subject: [PATCH 10/12] rename: NaiveAggregatedSignatures to NaiveAggregatedSignature --- packages/testing/src/consensus_testing/keys.py | 4 ++-- src/lean_spec/subspecs/containers/block/__init__.py | 4 ++-- src/lean_spec/subspecs/containers/block/types.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index e505b741..7c49ae11 100644 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -33,7 +33,7 @@ from lean_spec.subspecs.containers.block.types import ( AggregatedAttestations, AttestationSignatures, - NaiveAggregatedSignatures, + NaiveAggregatedSignature, ) from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root @@ -239,7 +239,7 @@ def build_attestation_signatures( """ return AttestationSignatures( data=[ - NaiveAggregatedSignatures( + NaiveAggregatedSignature( data=[ ( signature_lookup.get( diff --git a/src/lean_spec/subspecs/containers/block/__init__.py b/src/lean_spec/subspecs/containers/block/__init__.py index 819745d6..e16b3b80 100644 --- a/src/lean_spec/subspecs/containers/block/__init__.py +++ b/src/lean_spec/subspecs/containers/block/__init__.py @@ -11,7 +11,7 @@ from .types import ( AggregatedAttestations, AttestationSignatures, - NaiveAggregatedSignatures, + NaiveAggregatedSignature, ) __all__ = [ @@ -22,6 +22,6 @@ "BlockWithAttestation", "SignedBlockWithAttestation", "AggregatedAttestations", - "NaiveAggregatedSignatures", + "NaiveAggregatedSignature", "AttestationSignatures", ] diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index c6d62c38..738ef29f 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -14,7 +14,7 @@ class AggregatedAttestations(SSZList): LIMIT = int(VALIDATOR_REGISTRY_LIMIT) -class NaiveAggregatedSignatures(SSZList): +class NaiveAggregatedSignature(SSZList): """Aggregated signature list included alongside the block proposer's attestation.""" ELEMENT_TYPE = XmssSignature @@ -24,5 +24,5 @@ class NaiveAggregatedSignatures(SSZList): class AttestationSignatures(SSZList): """List of per-attestation naive signature lists aligned with block body attestations.""" - ELEMENT_TYPE = NaiveAggregatedSignatures + ELEMENT_TYPE = NaiveAggregatedSignature LIMIT = int(VALIDATOR_REGISTRY_LIMIT) From ce7d15278316b1b281155e99e1440fba28a4da05 Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Sat, 13 Dec 2025 23:28:33 +0530 Subject: [PATCH 11/12] refactor: move NaiveAggregatedSignature to attestation types --- packages/testing/src/consensus_testing/keys.py | 2 +- src/lean_spec/subspecs/containers/__init__.py | 4 ++-- .../subspecs/containers/attestation/__init__.py | 4 ++-- .../subspecs/containers/attestation/attestation.py | 4 ++-- src/lean_spec/subspecs/containers/attestation/types.py | 2 +- src/lean_spec/subspecs/containers/block/__init__.py | 2 -- src/lean_spec/subspecs/containers/block/types.py | 10 +--------- 7 files changed, 9 insertions(+), 19 deletions(-) diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index 7c49ae11..932a9c16 100644 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -30,10 +30,10 @@ from typing import TYPE_CHECKING, Iterator, Self from lean_spec.subspecs.containers import AttestationData +from lean_spec.subspecs.containers.attestation.types import NaiveAggregatedSignature from lean_spec.subspecs.containers.block.types import ( AggregatedAttestations, AttestationSignatures, - NaiveAggregatedSignature, ) from lean_spec.subspecs.containers.slot import Slot from lean_spec.subspecs.ssz.hash import hash_tree_root diff --git a/src/lean_spec/subspecs/containers/__init__.py b/src/lean_spec/subspecs/containers/__init__.py index d04aa685..0cf54111 100644 --- a/src/lean_spec/subspecs/containers/__init__.py +++ b/src/lean_spec/subspecs/containers/__init__.py @@ -10,10 +10,10 @@ from .attestation import ( AggregatedAttestation, - AggregatedSignatures, AggregationBits, Attestation, AttestationData, + NaiveAggregatedSignature, SignedAggregatedAttestation, SignedAttestation, ) @@ -31,7 +31,7 @@ __all__ = [ "AggregatedAttestation", - "AggregatedSignatures", + "NaiveAggregatedSignature", "AggregationBits", "AttestationData", "Attestation", diff --git a/src/lean_spec/subspecs/containers/attestation/__init__.py b/src/lean_spec/subspecs/containers/attestation/__init__.py index 70ea5ba8..7e05a530 100644 --- a/src/lean_spec/subspecs/containers/attestation/__init__.py +++ b/src/lean_spec/subspecs/containers/attestation/__init__.py @@ -7,7 +7,7 @@ SignedAggregatedAttestation, SignedAttestation, ) -from .types import AggregatedSignatures, AggregationBits +from .types import AggregationBits, NaiveAggregatedSignature __all__ = [ "AttestationData", @@ -15,6 +15,6 @@ "SignedAttestation", "SignedAggregatedAttestation", "AggregatedAttestation", - "AggregatedSignatures", + "NaiveAggregatedSignature", "AggregationBits", ] diff --git a/src/lean_spec/subspecs/containers/attestation/attestation.py b/src/lean_spec/subspecs/containers/attestation/attestation.py index e64d4588..c6614251 100644 --- a/src/lean_spec/subspecs/containers/attestation/attestation.py +++ b/src/lean_spec/subspecs/containers/attestation/attestation.py @@ -22,7 +22,7 @@ from ...xmss.containers import Signature from ..checkpoint import Checkpoint -from .types import AggregatedSignatures, AggregationBits +from .types import AggregationBits, NaiveAggregatedSignature class AttestationData(Container): @@ -129,7 +129,7 @@ class SignedAggregatedAttestation(Container): message: AggregatedAttestation """Aggregated attestation data.""" - signature: AggregatedSignatures + signature: NaiveAggregatedSignature """Aggregated attestation plus its combined signature. Stores a naive list of validator signatures that mirrors the attestation diff --git a/src/lean_spec/subspecs/containers/attestation/types.py b/src/lean_spec/subspecs/containers/attestation/types.py index 904a27be..9dec9bae 100644 --- a/src/lean_spec/subspecs/containers/attestation/types.py +++ b/src/lean_spec/subspecs/containers/attestation/types.py @@ -59,7 +59,7 @@ def to_validator_indices(self) -> list[Uint64]: return indices -class AggregatedSignatures(SSZList): +class NaiveAggregatedSignature(SSZList): """Naive list of validator signatures used for aggregation placeholders.""" ELEMENT_TYPE = Signature diff --git a/src/lean_spec/subspecs/containers/block/__init__.py b/src/lean_spec/subspecs/containers/block/__init__.py index e16b3b80..4ed7dfa7 100644 --- a/src/lean_spec/subspecs/containers/block/__init__.py +++ b/src/lean_spec/subspecs/containers/block/__init__.py @@ -11,7 +11,6 @@ from .types import ( AggregatedAttestations, AttestationSignatures, - NaiveAggregatedSignature, ) __all__ = [ @@ -22,6 +21,5 @@ "BlockWithAttestation", "SignedBlockWithAttestation", "AggregatedAttestations", - "NaiveAggregatedSignature", "AttestationSignatures", ] diff --git a/src/lean_spec/subspecs/containers/block/types.py b/src/lean_spec/subspecs/containers/block/types.py index 738ef29f..e602ef20 100644 --- a/src/lean_spec/subspecs/containers/block/types.py +++ b/src/lean_spec/subspecs/containers/block/types.py @@ -3,8 +3,7 @@ from lean_spec.types import SSZList from ...chain.config import VALIDATOR_REGISTRY_LIMIT -from ...xmss.containers import Signature as XmssSignature -from ..attestation import AggregatedAttestation +from ..attestation import AggregatedAttestation, NaiveAggregatedSignature class AggregatedAttestations(SSZList): @@ -14,13 +13,6 @@ class AggregatedAttestations(SSZList): LIMIT = int(VALIDATOR_REGISTRY_LIMIT) -class NaiveAggregatedSignature(SSZList): - """Aggregated signature list included alongside the block proposer's attestation.""" - - ELEMENT_TYPE = XmssSignature - LIMIT = int(VALIDATOR_REGISTRY_LIMIT) - - class AttestationSignatures(SSZList): """List of per-attestation naive signature lists aligned with block body attestations.""" From 9e05a53caca232897f5a2b46332d6fe53e9701f6 Mon Sep 17 00:00:00 2001 From: anshalshukla Date: Sun, 14 Dec 2025 04:44:34 +0530 Subject: [PATCH 12/12] fixes: nit comments --- .../testing/src/consensus_testing/keys.py | 38 ++++--------------- .../test_fixtures/verify_signatures.py | 2 +- .../subspecs/containers/block/block.py | 5 +-- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index 932a9c16..0dad9b0f 100644 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -36,7 +36,6 @@ AttestationSignatures, ) from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.subspecs.xmss.containers import PublicKey, SecretKey, Signature from lean_spec.subspecs.xmss.interface import TEST_SIGNATURE_SCHEME, GeneralizedXmssScheme from lean_spec.types import Uint64 @@ -210,7 +209,7 @@ def sign_attestation_data( self._state[validator_id] = kp.with_secret(sk) # Sign hash tree root of the attestation data - message = bytes(hash_tree_root(attestation_data)) + message = attestation_data.data_root_bytes() return self.scheme.sign(sk, epoch, message) def build_attestation_signatures( @@ -218,43 +217,20 @@ def build_attestation_signatures( aggregated_attestations: AggregatedAttestations, signature_lookup: Mapping[tuple[Uint64, bytes], Signature] | None = None, ) -> AttestationSignatures: - """ - Build `AttestationSignatures` for already-aggregated attestations. - - This is a convenience helper for tests/fixtures that need to produce - `BlockSignatures.attestation_signatures` for a block. - - Args: - aggregated_attestations: Iterable of aggregated attestation containers. - Each item is expected to have: - - `.data` (AttestationData) - - `.aggregation_bits.to_validator_indices()` (Iterable[Uint64]) - signature_lookup: Optional override map keyed by - `(validator_id, bytes(hash_tree_root(attestation_data))) -> signature`. - When provided and a key exists, that signature is used instead of signing. - - Returns: - AttestationSignatures matching the ordering of `aggregated_attestations` - and per-attestation validator index ordering. - """ + """Build `AttestationSignatures` for already-aggregated attestations.""" + lookup = signature_lookup or {} return AttestationSignatures( data=[ NaiveAggregatedSignature( data=[ ( - signature_lookup.get( - (validator_id, aggregated_attestation.data.data_root_bytes()) - ) - if signature_lookup is not None - else None - ) - or self.sign_attestation_data(validator_id, aggregated_attestation.data) - for validator_id in ( - aggregated_attestation.aggregation_bits.to_validator_indices() + lookup.get((vid, agg.data.data_root_bytes())) + or self.sign_attestation_data(vid, agg.data) ) + for vid in agg.aggregation_bits.to_validator_indices() ] ) - for aggregated_attestation in aggregated_attestations + for agg in aggregated_attestations ] ) 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 eaf0e1b4..dca4bbd3 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -210,7 +210,7 @@ def _build_block_from_spec( # for any intentionally-invalid signature from the input spec remains invalid # in the produced `SignedBlockWithAttestation`. signature_lookup: dict[tuple[Uint64, bytes], Signature] = { - (att.validator_id, bytes(hash_tree_root(att.data))): sig + (att.validator_id, att.data.data_root_bytes()): sig for att, sig in zip(attestations, attestation_signature_inputs, strict=True) } attestation_signatures = key_manager.build_attestation_signatures( diff --git a/src/lean_spec/subspecs/containers/block/block.py b/src/lean_spec/subspecs/containers/block/block.py index d4f99e1a..c1eef772 100644 --- a/src/lean_spec/subspecs/containers/block/block.py +++ b/src/lean_spec/subspecs/containers/block/block.py @@ -12,7 +12,6 @@ from typing import TYPE_CHECKING, cast from lean_spec.subspecs.containers.slot import Slot -from lean_spec.subspecs.ssz.hash import hash_tree_root from lean_spec.types import Bytes32, Uint64 from lean_spec.types.container import Container @@ -179,7 +178,7 @@ def verify_signatures(self, parent_state: "State") -> bool: "Aggregated attestation signature count mismatch" ) - attestation_root = bytes(hash_tree_root(aggregated_attestation.data)) + attestation_root = aggregated_attestation.data.data_root_bytes() # Verify each validator's attestation signature for validator_id, signature in zip(validator_ids, aggregated_signature, strict=True): @@ -204,7 +203,7 @@ def verify_signatures(self, parent_state: "State") -> bool: assert proposer_signature.verify( proposer.get_pubkey(), proposer_attestation.data.slot, - bytes(hash_tree_root(proposer_attestation.data)), + proposer_attestation.data.data_root_bytes(), ), "Proposer signature verification failed" return True