diff --git a/eth_validator_watcher/beacon.py b/eth_validator_watcher/beacon.py index 17eecbc..72286b4 100644 --- a/eth_validator_watcher/beacon.py +++ b/eth_validator_watcher/beacon.py @@ -17,6 +17,7 @@ BlockIdentierType, Committees, Genesis, + Spec, Header, ProposerDuties, Rewards, @@ -120,6 +121,15 @@ def get_genesis(self) -> Genesis: genesis_dict = response.json() return Genesis(**genesis_dict) + def get_spec(self) -> Spec: + """Get network specification.""" + response = self.__get_retry_not_found( + f"{self.__url}/eth/v1/config/spec", timeout=TIMEOUT_BEACON_SEC + ) + response.raise_for_status() + spec_dict = response.json() + return Spec(**spec_dict) + def get_header(self, block_identifier: Union[BlockIdentierType, int]) -> Header: """Get a header. @@ -129,7 +139,8 @@ def get_header(self, block_identifier: Union[BlockIdentierType, int]) -> Header: """ try: response = self.__get( - f"{self.__url}/eth/v1/beacon/headers/{block_identifier}", timeout=TIMEOUT_BEACON_SEC + f"{self.__url}/eth/v1/beacon/headers/{block_identifier}", + timeout=TIMEOUT_BEACON_SEC, ) response.raise_for_status() @@ -176,7 +187,8 @@ def get_proposer_duties(self, epoch: int) -> ProposerDuties: epoch: Epoch corresponding to the proposer duties to retrieve """ response = self.__get_retry_not_found( - f"{self.__url}/eth/v1/validator/duties/proposer/{epoch}", timeout=TIMEOUT_BEACON_SEC + f"{self.__url}/eth/v1/validator/duties/proposer/{epoch}", + timeout=TIMEOUT_BEACON_SEC, ) response.raise_for_status() @@ -193,7 +205,8 @@ def get_status_to_index_to_validator( inner value : Validator """ response = self.__get_retry_not_found( - f"{self.__url}/eth/v1/beacon/states/head/validators", timeout=TIMEOUT_BEACON_SEC + f"{self.__url}/eth/v1/beacon/states/head/validators", + timeout=TIMEOUT_BEACON_SEC, ) response.raise_for_status() diff --git a/eth_validator_watcher/entrypoint.py b/eth_validator_watcher/entrypoint.py index a79c7bc..9308d69 100644 --- a/eth_validator_watcher/entrypoint.py +++ b/eth_validator_watcher/entrypoint.py @@ -30,8 +30,6 @@ from .utils import ( CHUCK_NORRIS, MISSED_BLOCK_TIMEOUT_SEC, - NB_SECOND_PER_SLOT, - NB_SLOT_PER_EPOCH, SLOT_FOR_MISSED_ATTESTATIONS_PROCESS, SLOT_FOR_REWARDS_PROCESS, LimitedDict, @@ -246,9 +244,18 @@ def _handler( genesis = beacon.get_genesis() - for idx, (slot, slot_start_time_sec) in enumerate(slots(genesis.data.genesis_time)): + spec = beacon.get_spec() + seconds_per_slot = spec.data.SECONDS_PER_SLOT + slots_per_epoch = spec.data.SLOTS_PER_EPOCH + + for idx, (slot, slot_start_time_sec) in enumerate( + slots( + genesis.data.genesis_time, + seconds_per_slot=seconds_per_slot, + ) + ): if slot < 0: - chain_start_in_sec = -slot * NB_SECOND_PER_SLOT + chain_start_in_sec = -slot * seconds_per_slot days, hours, minutes, seconds = convert_seconds_to_dhms(chain_start_in_sec) print( @@ -256,7 +263,7 @@ def _handler( f"{minutes:2} minutes and {seconds:2} seconds." ) - if slot % NB_SLOT_PER_EPOCH == 0: + if slot % slots_per_epoch == 0: print(f"💪 {CHUCK_NORRIS[slot%len(CHUCK_NORRIS)]}") if liveness_file is not None: @@ -264,8 +271,8 @@ def _handler( continue - epoch = slot // NB_SLOT_PER_EPOCH - slot_in_epoch = slot % NB_SLOT_PER_EPOCH + epoch = slot // slots_per_epoch + slot_in_epoch = slot % slots_per_epoch metric_slot_gauge.set(slot) metric_epoch_gauge.set(epoch) @@ -389,10 +396,21 @@ def _handler( last_rewards_process_epoch = epoch - process_future_blocks_proposal(beacon, our_pubkeys, slot, is_new_epoch) + process_future_blocks_proposal( + beacon, + our_pubkeys, + slot, + is_new_epoch, + slots_per_epoch=slots_per_epoch, + ) last_processed_finalized_slot = process_missed_blocks_finalized( - beacon, last_processed_finalized_slot, slot, our_pubkeys, slack + beacon, + last_processed_finalized_slot, + slot, + our_pubkeys, + slack, + slots_per_epoch=slots_per_epoch, ) delta_sec = MISSED_BLOCK_TIMEOUT_SEC - (time() - slot_start_time_sec) @@ -408,10 +426,16 @@ def _handler( block, slot, our_active_idx2val, + slots_per_epoch=slots_per_epoch, ) process_fee_recipient( - block, our_active_idx2val, execution, fee_recipient, slack + block, + our_active_idx2val, + execution, + fee_recipient, + slack, + slots_per_epoch=slots_per_epoch, ) is_our_validator = process_missed_blocks_head( @@ -420,6 +444,7 @@ def _handler( slot, our_pubkeys, slack, + slots_per_epoch=slots_per_epoch, ) if is_our_validator and potential_block is not None: diff --git a/eth_validator_watcher/fee_recipient.py b/eth_validator_watcher/fee_recipient.py index d459a5a..8b9ebc2 100644 --- a/eth_validator_watcher/fee_recipient.py +++ b/eth_validator_watcher/fee_recipient.py @@ -19,6 +19,7 @@ def process_fee_recipient( execution: Execution | None, expected_fee_recipient: str | None, slack: Slack | None, + slots_per_epoch: int = NB_SLOT_PER_EPOCH, ) -> None: """Check if the fee recipient is the one expected. @@ -44,7 +45,7 @@ def process_fee_recipient( short_proposer_pubkey = index_to_validator[proposer_index].pubkey[:10] slot = block.data.message.slot - epoch = slot // NB_SLOT_PER_EPOCH + epoch = slot // slots_per_epoch # First, we check if the beacon block fee recipient is the one expected # `.lower()` is here just in case the execution client returns a fee recipient in diff --git a/eth_validator_watcher/missed_blocks.py b/eth_validator_watcher/missed_blocks.py index 5bfebd5..5d18a0b 100644 --- a/eth_validator_watcher/missed_blocks.py +++ b/eth_validator_watcher/missed_blocks.py @@ -27,6 +27,7 @@ def process_missed_blocks_head( slot: int, our_pubkeys: set[str], slack: Slack | None, + slots_per_epoch: int = NB_SLOT_PER_EPOCH, ) -> bool: """Process missed block proposals detection at head @@ -40,7 +41,7 @@ def process_missed_blocks_head( Returns `True` if we had to propose the block, `False` otherwise """ missed = potential_block is None - epoch = slot // NB_SLOT_PER_EPOCH + epoch = slot // slots_per_epoch proposer_duties = beacon.get_proposer_duties(epoch) # Get proposer public key for this slot @@ -98,6 +99,7 @@ def process_missed_blocks_finalized( slot: int, our_pubkeys: set[str], slack: Slack | None, + slots_per_epoch: int = NB_SLOT_PER_EPOCH, ) -> int: """Process missed block proposals detection at finalized @@ -114,14 +116,14 @@ def process_missed_blocks_finalized( last_finalized_header = beacon.get_header(BlockIdentierType.FINALIZED) last_finalized_slot = last_finalized_header.data.header.message.slot - epoch_of_last_finalized_slot = last_finalized_slot // NB_SLOT_PER_EPOCH + epoch_of_last_finalized_slot = last_finalized_slot // slots_per_epoch # Only to memoize it, in case of the BN does not serve this request for too old # epochs beacon.get_proposer_duties(epoch_of_last_finalized_slot) for slot_ in range(last_processed_finalized_slot + 1, last_finalized_slot + 1): - epoch = slot_ // NB_SLOT_PER_EPOCH + epoch = slot_ // slots_per_epoch proposer_duties = beacon.get_proposer_duties(epoch) # Get proposer public key for this slot diff --git a/eth_validator_watcher/models.py b/eth_validator_watcher/models.py index 7e72d64..cd1177c 100644 --- a/eth_validator_watcher/models.py +++ b/eth_validator_watcher/models.py @@ -38,6 +38,14 @@ class Data(BaseModel): data: Data +class Spec(BaseModel): + class Data(BaseModel): + SECONDS_PER_SLOT: int + SLOTS_PER_EPOCH: int + + data: Data + + class Header(BaseModel): class Data(BaseModel): class Header(BaseModel): diff --git a/eth_validator_watcher/next_blocks_proposal.py b/eth_validator_watcher/next_blocks_proposal.py index 3a06fe5..ce0b3e9 100644 --- a/eth_validator_watcher/next_blocks_proposal.py +++ b/eth_validator_watcher/next_blocks_proposal.py @@ -20,6 +20,7 @@ def process_future_blocks_proposal( our_pubkeys: set[str], slot: int, is_new_epoch: bool, + slots_per_epoch: int = NB_SLOT_PER_EPOCH, ) -> int: """Handle next blocks proposal @@ -29,7 +30,7 @@ def process_future_blocks_proposal( slot : Slot is_new_epoch: Is new epoch """ - epoch = slot // NB_SLOT_PER_EPOCH + epoch = slot // slots_per_epoch proposers_duties_current_epoch = beacon.get_proposer_duties(epoch) proposers_duties_next_epoch = beacon.get_proposer_duties(epoch + 1) diff --git a/eth_validator_watcher/suboptimal_attestations.py b/eth_validator_watcher/suboptimal_attestations.py index cc528de..809994c 100644 --- a/eth_validator_watcher/suboptimal_attestations.py +++ b/eth_validator_watcher/suboptimal_attestations.py @@ -29,6 +29,7 @@ def process_suboptimal_attestations( block: Block, slot: int, our_active_validators_index_to_validator: dict[int, Validators.DataItem.Validator], + slots_per_epoch: int = NB_SLOT_PER_EPOCH, ) -> set[int]: """Process sub-optimal attestations @@ -48,7 +49,7 @@ def process_suboptimal_attestations( # Epoch of previous slot is NOT the previous epoch, but really the epoch # corresponding to the previous slot. - epoch_of_previous_slot = previous_slot // NB_SLOT_PER_EPOCH + epoch_of_previous_slot = previous_slot // slots_per_epoch # All our active validators index our_active_validators_index = set(our_active_validators_index_to_validator) @@ -133,7 +134,9 @@ def process_suboptimal_attestations( ) if suboptimal_attestations_rate is not None: - metric_suboptimal_attestations_rate_gauge.set(100 * suboptimal_attestations_rate) + metric_suboptimal_attestations_rate_gauge.set( + 100 * suboptimal_attestations_rate + ) if len(our_validators_index_that_did_not_attest_optimally_during_previous_slot) > 0: assert suboptimal_attestations_rate is not None diff --git a/eth_validator_watcher/utils.py b/eth_validator_watcher/utils.py index 074a3ad..689a770 100644 --- a/eth_validator_watcher/utils.py +++ b/eth_validator_watcher/utils.py @@ -231,12 +231,15 @@ def send_message(self, message: str) -> None: self.__client.chat_postMessage(channel=self.__channel, text=message) -def slots(genesis_time_sec: int) -> Iterator[Tuple[int, int]]: - next_slot = int((time() - genesis_time_sec) / NB_SECOND_PER_SLOT) + 1 +def slots( + genesis_time_sec: int, + seconds_per_slot: int = NB_SECOND_PER_SLOT, +) -> Iterator[Tuple[int, int]]: + next_slot = int((time() - genesis_time_sec) / seconds_per_slot) + 1 try: while True: - next_slot_time_sec = genesis_time_sec + next_slot * NB_SECOND_PER_SLOT + next_slot_time_sec = genesis_time_sec + next_slot * seconds_per_slot time_to_wait = next_slot_time_sec - time() sleep(max(0, time_to_wait)) diff --git a/tests/entrypoint/test__handler.py b/tests/entrypoint/test__handler.py index d960d47..d7f6be2 100644 --- a/tests/entrypoint/test__handler.py +++ b/tests/entrypoint/test__handler.py @@ -4,7 +4,7 @@ from eth_validator_watcher import entrypoint from eth_validator_watcher.entrypoint import _handler -from eth_validator_watcher.models import BeaconType, Genesis, Validators +from eth_validator_watcher.models import BeaconType, Genesis, Validators, Spec from eth_validator_watcher.utils import LimitedDict, Slack from eth_validator_watcher.web3signer import Web3Signer from freezegun import freeze_time @@ -72,11 +72,19 @@ def get_genesis(self) -> Genesis: ) ) + def get_spec(self) -> Spec: + return Spec( + data=Spec.Data( + SECONDS_PER_SLOT=12, + SLOTS_PER_EPOCH=32, + ) + ) + def get_our_pubkeys(pubkeys_file_path: Path, web3signer: None) -> set[str]: assert pubkeys_file_path == Path("/path/to/pubkeys") raise ValueError("Invalid pubkeys") - def slots(genesis_time: int) -> Iterator[Tuple[(int, int)]]: + def slots(genesis_time: int, seconds_per_slot=12) -> Iterator[Tuple[(int, int)]]: assert genesis_time == 0 yield 63, 1664 yield 64, 1676 @@ -115,10 +123,18 @@ def get_genesis(self) -> Genesis: ) ) + def get_spec(self) -> Spec: + return Spec( + data=Spec.Data( + SECONDS_PER_SLOT=12, + SLOTS_PER_EPOCH=32, + ) + ) + def get_our_pubkeys(pubkeys_file_path: Path, web3signer: None) -> set[str]: return {"0x12345", "0x67890"} - def slots(genesis_time: int) -> Iterator[Tuple[(int, int)]]: + def slots(genesis_time: int, seconds_per_slot=12) -> Iterator[Tuple[(int, int)]]: assert genesis_time == 0 yield -32, 1664 @@ -165,6 +181,14 @@ def get_genesis(self) -> Genesis: ) ) + def get_spec(self) -> Spec: + return Spec( + data=Spec.Data( + SECONDS_PER_SLOT=12, + SLOTS_PER_EPOCH=32, + ) + ) + def get_status_to_index_to_validator( self, ) -> dict[StatusEnum, dict[int, Validator]]: @@ -216,7 +240,7 @@ def __init__(self, urls: list[str]) -> None: def process(self, slot: int) -> None: assert slot in {63, 64} - def slots(genesis_time: int) -> Iterator[Tuple[(int, int)]]: + def slots(genesis_time: int, seconds_per_slot=12) -> Iterator[Tuple[(int, int)]]: assert genesis_time == 0 yield 63, 1664 yield 64, 1676 @@ -264,7 +288,11 @@ def process_double_missed_attestations( return {4} def process_future_blocks_proposal( - beacon: Beacon, pubkeys: set[str], slot: int, is_new_epoch: bool + beacon: Beacon, + pubkeys: set[str], + slot: int, + is_new_epoch: bool, + slots_per_epoch: int = 32, ) -> int: assert isinstance(beacon, Beacon) assert pubkeys == {"0xaaa", "0xbbb", "0xccc", "0xddd", "0xeee", "0xfff"} @@ -279,6 +307,7 @@ def process_missed_blocks_finalized( slot: int, pubkeys: set[str], slack: Slack, + slots_per_epoch: int = 32, ) -> int: assert isinstance(beacon, Beacon) assert last_processed_finalized_slot == 63 @@ -293,6 +322,7 @@ def process_suboptimal_attestations( potential_block: str | None, slot: int, index_to_validator: dict[int, Validator], + slots_per_epoch: int = 32, ) -> set[int]: assert isinstance(beacon, Beacon) assert potential_block == "A BLOCK" @@ -311,6 +341,7 @@ def process_missed_blocks( slot: int, pubkeys: set[str], slack: Slack, + slots_per_epoch: int = 32, ) -> bool: assert isinstance(beacon, Beacon) assert potential_block == "A BLOCK"