diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index ba6f94a2c..0d00a858e 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -7,7 +7,7 @@ from src import variables from src.constants import SLOTS_PER_HISTORICAL_ROOT -from src.metrics.prometheus.csm import CSM_UNPROCESSED_EPOCHS_COUNT, CSM_MIN_UNPROCESSED_EPOCH +from src.metrics.prometheus.csm import CSM_MIN_UNPROCESSED_EPOCH, CSM_UNPROCESSED_EPOCHS_COUNT from src.modules.csm.state import State from src.providers.consensus.client import ConsensusClient from src.providers.consensus.types import BlockAttestation @@ -20,8 +20,7 @@ lock = Lock() -class MinStepIsNotReached(Exception): - ... +class MinStepIsNotReached(Exception): ... @dataclass @@ -103,7 +102,9 @@ def _is_min_step_reached(self): return False -type Committees = dict[tuple[str, str], list[ValidatorDuty]] +type Slot = str +type CommitteeIndex = str +type Committees = dict[tuple[Slot, CommitteeIndex], list[ValidatorDuty]] class FrameCheckpointProcessor: @@ -193,7 +194,10 @@ def _check_duty( committees = self._prepare_committees(EpochNumber(duty_epoch)) for root in block_roots: attestations = self.cc.get_block_attestations(BlockRoot(root)) - process_attestations(attestations, committees) + if attestations and is_pectra_attestation(attestations[0]): + process_attestations_electra(attestations, committees) + else: + process_attestations(attestations, committees) with lock: for committee in committees.values(): @@ -227,11 +231,24 @@ def _prepare_committees(self, epoch: EpochNumber) -> Committees: return committees +def process_attestations_electra(attestations: Iterable[BlockAttestation], committees: Committees) -> None: + for attestation in attestations: + assert is_pectra_attestation(attestation) + committee_offset = 0 + for committee_idx in get_committee_indices(attestation): + committee = committees.get((attestation.data.slot, committee_idx), []) + att_bits = hex_bitvector_to_list(attestation.aggregation_bits)[committee_offset:][: len(committee)] + for index_in_committee, validator_duty in enumerate(committee): + validator_duty.included = validator_duty.included or _is_attested(att_bits, index_in_committee) + committee_offset += len(committee) + + def process_attestations(attestations: Iterable[BlockAttestation], committees: Committees) -> None: for attestation in attestations: + assert not is_pectra_attestation(attestation) committee_id = (attestation.data.slot, attestation.data.index) committee = committees.get(committee_id, []) - att_bits = _to_bits(attestation.aggregation_bits) + att_bits = hex_bitvector_to_list(attestation.aggregation_bits) for index_in_committee, validator_duty in enumerate(committee): validator_duty.included = validator_duty.included or _is_attested(att_bits, index_in_committee) @@ -240,7 +257,15 @@ def _is_attested(bits: Sequence[bool], index: int) -> bool: return bits[index] -def _to_bits(aggregation_bits: str) -> Sequence[bool]: +def is_pectra_attestation(attestation: BlockAttestation) -> bool: + return attestation.committee_bits != "" and attestation.data.index == "0" + + +def get_committee_indices(attestation: BlockAttestation) -> list[CommitteeIndex]: + return [str(idx) for (idx, bit) in enumerate(hex_bitvector_to_list(attestation.committee_bits)) if bit] + + +def hex_bitvector_to_list(bitvector: str) -> list[bool]: # copied from https://github.com/ethereum/py-ssz/blob/main/ssz/sedes/bitvector.py#L66 - att_bytes = bytes.fromhex(aggregation_bits[2:]) - return [bool((att_bytes[bit_index // 8] >> bit_index % 8) % 2) for bit_index in range(len(att_bytes) * 8)] + bytes_ = bytes.fromhex(bitvector[2:]) if bitvector.startswith("0x") else bytes.fromhex(bitvector) + return [bool((bytes_[bit_index // 8] >> bit_index % 8) % 2) for bit_index in range(len(bytes_) * 8)] diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index 0c39d2f64..9c20e1007 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -124,7 +124,7 @@ def get_attestation_committees( self, blockstamp: BlockStamp, epoch: EpochNumber | None = None, - index: int | None = None, + index: int | None = None, # committee index slot: SlotNumber | None = None ) -> list[SlotAttestationCommittee]: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees""" diff --git a/src/providers/consensus/types.py b/src/providers/consensus/types.py index 7b257b34c..33d5ff71d 100644 --- a/src/providers/consensus/types.py +++ b/src/providers/consensus/types.py @@ -75,7 +75,7 @@ class Checkpoint: @dataclass class AttestationData(Nested, FromResponse): slot: str - index: str + index: str # CommitteeIndex before Electra or 0 afterward. beacon_block_root: BlockRoot source: Checkpoint target: Checkpoint @@ -85,6 +85,7 @@ class AttestationData(Nested, FromResponse): class BlockAttestation(Nested, FromResponse): aggregation_bits: str data: AttestationData + committee_bits: str = "" # [New in Electra:EIP7549] @dataclass