Skip to content

Commit

Permalink
feat: add basic handling of the main processing loop and introduced w…
Browse files Browse the repository at this point in the history
…atched validators
  • Loading branch information
aimxhaisse committed Apr 4, 2024
1 parent c23d6c1 commit 5956b36
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 9 deletions.
4 changes: 4 additions & 0 deletions etc/config.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ slack_token: ~
relays: ~
liveness_file: ~

# This mapping is reloaded dynamically at the beginning of each
# epoch. If the new mapping is invalid the watcher will crash, be sure
# to use atomic filesystem operations to have a completely updated
# configuration file if you dynamically watch keys.
watched_keys:
- public_key: '0x832b8286f5d6535fd941c6c4ed8b9b20d214fc6aa726ce4fba1c9dbb4f278132646304f550e557231b6932aa02cf08d3'
labels: ['google']
Expand Down
23 changes: 23 additions & 0 deletions eth_validator_watcher/beacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ def __init__(self, url: str, timeout_sec: int) -> None:
self.__http.mount("http://", adapter)
self.__http.mount("https://", adapter)

def get_url(self) -> str:
"""Return the URL of the beacon."""
return self.__url

Check warning on line 87 in eth_validator_watcher/beacon.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/beacon.py#L87

Added line #L87 was not covered by tests

def get_timeout_sec(self) -> int:
"""Return the timeout in seconds used to query the beacon."""
return self.__timeout_sec

Check warning on line 91 in eth_validator_watcher/beacon.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/beacon.py#L91

Added line #L91 was not covered by tests

@retry(
stop=stop_after_attempt(5),
wait=wait_fixed(3),
Expand Down Expand Up @@ -208,6 +216,21 @@ def get_status_to_index_to_validator(

return result

def get_validators(self) -> Validators:
response = self.__get_retry_not_found(

Check warning on line 220 in eth_validator_watcher/beacon.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/beacon.py#L220

Added line #L220 was not covered by tests
f"{self.__url}/eth/v1/beacon/states/head/validators", timeout=self.__timeout_sec
)

# Unsure if explicit del help with memory here, let's keep it
# for now and benchmark this in real conditions.
response.raise_for_status()
validators_dict = response.json()
del response
validators = Validators(**validators_dict)
del validators_dict

Check warning on line 230 in eth_validator_watcher/beacon.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/beacon.py#L226-L230

Added lines #L226 - L230 were not covered by tests

return validators

Check warning on line 232 in eth_validator_watcher/beacon.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/beacon.py#L232

Added line #L232 was not covered by tests

@lru_cache(maxsize=1)
def get_duty_slot_to_committee_index_to_validators_index(
self, epoch: int
Expand Down
34 changes: 27 additions & 7 deletions eth_validator_watcher/entrypoint_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from pathlib import Path
from typing import Optional

Check warning on line 6 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L4-L6

Added lines #L4 - L6 were not covered by tests

import logging
import typer
import time

Check warning on line 10 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L8-L10

Added lines #L8 - L10 were not covered by tests

from .beacon import Beacon
from .config import load_config, WatchedKeyConfig
from .watched_validators import WatchedValidators

Check warning on line 14 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L12-L14

Added lines #L12 - L14 were not covered by tests


print = partial(print, flush=True)
app = typer.Typer(add_completion=False)

Check warning on line 17 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L17

Added line #L17 was not covered by tests


Expand All @@ -27,23 +29,37 @@ def __init__(self, cfg_path: Path) -> None:
cfg_path: Path
Path to the configuration file.
"""
self.cfg_path = cfg_path
self.cfg = None
self._cfg_path = cfg_path
self._cfg = None
self._beacon = None

Check warning on line 34 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L32-L34

Added lines #L32 - L34 were not covered by tests

def reload_config(self) -> None:
self._reload_config()

Check warning on line 36 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L36

Added line #L36 was not covered by tests

def _reload_config(self) -> None:

Check warning on line 38 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L38

Added line #L38 was not covered by tests
"""Reload the configuration file.
"""
try:
self.cfg = load_config(self.cfg_path)
self._cfg = load_config(self._cfg_path)
except ValidationError as err:
raise typer.BadParameter(f'Invalid configuration file: {err}')

Check warning on line 44 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L41-L44

Added lines #L41 - L44 were not covered by tests

if self._beacon is None or self._beacon.get_url() != self._cfg.beacon_url or self._beacon.get_timeout_sec() != self._cfg.beacon_timeout_sec:
self._beacon = Beacon(self._cfg.beacon_url, self._cfg.beacon_timeout_sec)

Check warning on line 47 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L46-L47

Added lines #L46 - L47 were not covered by tests

def run(self) -> None:

Check warning on line 49 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L49

Added line #L49 was not covered by tests
"""Run the Ethereum Validator Watcher.
"""
watched_validators = WatchedValidators()
while True:
print('Reloading configuration...')
self.reload_config()
logging.info('Processing new epoch')
beacon_validators = self._beacon.get_validators()
watched_validators.process_epoch(beacon_validators)

Check warning on line 56 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L52-L56

Added lines #L52 - L56 were not covered by tests

logging.info('Processing configuration update')
self._reload_config()
watched_validators.process_config(self._cfg)

Check warning on line 60 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L58-L60

Added lines #L58 - L60 were not covered by tests

logging.info('Waiting for next iteration')
time.sleep(1)

Check warning on line 63 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L62-L63

Added lines #L62 - L63 were not covered by tests


Expand All @@ -60,6 +76,10 @@ def handler(
),
) -> None:
"""Run the Ethereum Validator Watcher."""
logging.basicConfig(

Check warning on line 79 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L79

Added line #L79 was not covered by tests
level=logging.INFO,
format='%(asctime)s %(levelname)-8s %(message)s'
)

watcher = ValidatorWatcher(config)
watcher.run()

Check warning on line 85 in eth_validator_watcher/entrypoint_v2.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/entrypoint_v2.py#L84-L85

Added lines #L84 - L85 were not covered by tests
162 changes: 162 additions & 0 deletions eth_validator_watcher/watched_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
"""Watched validators.
This module provides a wrapper around per-validator computations
before exposing them later to prometheus. There are 4 types of
processing performed:
- process_config: configuration update (per-key labels)
- process_epoch: new epoch processing (beacon chain status update)
- process_missed_attestations: missed attestation processing (slot 16)
- process_rewards: rewards processing (slot 17)
WatchedValidator which holds the state of a validator while
WatchedValidators handles the collection of all validators, providing
efficient ways to access them which are then used by the prometheus
exporter.
"""

import logging

Check warning on line 18 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L18

Added line #L18 was not covered by tests

from typing import Optional

Check warning on line 20 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L20

Added line #L20 was not covered by tests

from .config import Config, WatchedKeyConfig
from .models import Validators

Check warning on line 23 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L22-L23

Added lines #L22 - L23 were not covered by tests


class WatchedValidator:

Check warning on line 26 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L26

Added line #L26 was not covered by tests
"""Watched validator abstraction.
This needs to be optimized for both CPU and memory usage as it
will be instantiated for every validator of the network.
Attributes:
index: Validator index
pubkey: Validator public key
status: Validator status for the current epoch
previous_status: Validator previous status for the previous epoch
labels: Validator labels
missed_attestation: Validator missed attestation for the current epoch
previous_missed_attestation: Validator missed previous attestation for the previous epoch
suboptimal_source: Validator suboptimal source for the current epoch
suboptimal_target: Validator suboptimal target for the current epoch
suboptimal_head: Validator suboptimal head for the current epoch
beacon_validator: Latest state of the validator from the beacon chain
"""

def __init__(self):
self.index : int = 0
self.previous_status : Validators.DataItem.StatusEnum | None = None
self.labels : Optional[list[str]] = None
self.missed_attestation : bool | None = None
self.previous_missed_attestation : bool | None = None
self.suboptimal_source : bool | None = None
self.suboptimal_target : bool | None = None
self.suboptimal_head : bool | None = None
self.beacon_validator : Validators.DataItem | None = None

Check warning on line 55 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L46-L55

Added lines #L46 - L55 were not covered by tests

@property
def pubkey(self) -> str:
return self.beacon_validator.validator.pubkey

Check warning on line 59 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L57-L59

Added lines #L57 - L59 were not covered by tests

@property
def status(self) -> Validators.DataItem.StatusEnum:
return self.beacon_validator.status

Check warning on line 63 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L61-L63

Added lines #L61 - L63 were not covered by tests

def process_config(self, config: WatchedKeyConfig):

Check warning on line 65 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L65

Added line #L65 was not covered by tests
"""Processes a new configuration.
Parameters:
config: New configuration
"""
self.labels = config.labels

Check warning on line 71 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L71

Added line #L71 was not covered by tests

def process_epoch(self, validator: Validators.DataItem):

Check warning on line 73 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L73

Added line #L73 was not covered by tests
"""Processes a new epoch.
Parameters:
validator: Validator beacon state
"""
if self.beacon_validator is not None:
self.previous_status = self.status

Check warning on line 80 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L79-L80

Added lines #L79 - L80 were not covered by tests

self.previous_missed_attestation = self.missed_attestation
self.missed_attestation = None

Check warning on line 83 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L82-L83

Added lines #L82 - L83 were not covered by tests

self.suboptimal_source = None
self.suboptimal_target = None
self.suboptimal_head = None

Check warning on line 87 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L85-L87

Added lines #L85 - L87 were not covered by tests

self.beacon_validator = validator

Check warning on line 89 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L89

Added line #L89 was not covered by tests


class WatchedValidators:

Check warning on line 92 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L92

Added line #L92 was not covered by tests
"""Wrapper around watched validators.
Provides facilities to retrieve a validator by index or public
key. This needs to be efficient both in terms of CPU and memory as
there are about ~1 million validators on the network.
"""

def __init__(self):
self._validators: dict[int, WatchedValidator] = {}
self._pubkey_to_index: dict[str, int] = {}

Check warning on line 102 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L100-L102

Added lines #L100 - L102 were not covered by tests

def get_validator_by_index(self, index: int) -> Optional[WatchedValidator]:

Check warning on line 104 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L104

Added line #L104 was not covered by tests
"""Get a validator by index.
Parameters:
index: Index of the validator to retrieve
"""
return self._validators.get(index)

Check warning on line 110 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L110

Added line #L110 was not covered by tests

def get_validator_by_pubkey(self, pubkey: str) -> Optional[WatchedValidator]:

Check warning on line 112 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L112

Added line #L112 was not covered by tests
"""Get a validator by public key.
Parameters:
pubkey: Public key of the validator to retrieve
"""
index = self._pubkey_to_index.get(pubkey)
if index is None:
return None
return self._validators.get(index)

Check warning on line 121 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L118-L121

Added lines #L118 - L121 were not covered by tests

def process_config(self, config: Config):

Check warning on line 123 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L123

Added line #L123 was not covered by tests
"""Process a config update
Parameters:
config: Updated configuration
"""
logging.info('Processing config & validator labels')

Check warning on line 129 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L129

Added line #L129 was not covered by tests

unknown = 0
for item in config.watched_keys:
updated = False
index = self._pubkey_to_index.get(item.public_key, None)
if index:
validator = self._validators.get(index)
if validator:
validator.process_config(item)
updated = True
if not updated:
unknown += 1

Check warning on line 141 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L131-L141

Added lines #L131 - L141 were not covered by tests

logging.info(f'Config processed ({unknown} unknown validators were skipped)')

Check warning on line 143 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L143

Added line #L143 was not covered by tests

def process_epoch(self, validators: Validators):

Check warning on line 145 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L145

Added line #L145 was not covered by tests
"""Process a new epoch
Parameters:
validators: New validator state for the epoch from the beaconchain.
"""
logging.info('Processing new epoch')

Check warning on line 151 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L151

Added line #L151 was not covered by tests

for item in validators.data:
validator = self._validators.get(item.index)
if validator is None:
validator = WatchedValidator()
self._validators[item.index] = validator
self._pubkey_to_index[item.validator.pubkey] = item.index

Check warning on line 158 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L153-L158

Added lines #L153 - L158 were not covered by tests

validator.process_epoch(item)

Check warning on line 160 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L160

Added line #L160 was not covered by tests

logging.info(f'New epoch processed ({len(validators.data)} validators)')

Check warning on line 162 in eth_validator_watcher/watched_validators.py

View check run for this annotation

Codecov / codecov/patch

eth_validator_watcher/watched_validators.py#L162

Added line #L162 was not covered by tests
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.scripts]
eth-validator-watcher = "eth_validator_watcher.entrypoint:app"
eth-validator-watcher = "eth_validator_watcher.entrypoint_v2:app"

0 comments on commit 5956b36

Please sign in to comment.