diff --git a/README.md b/README.md index 2d72602..afeb358 100644 --- a/README.md +++ b/README.md @@ -56,32 +56,34 @@ Command line options -------------------- ``` -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ * --beacon-url TEXT URL of beacon node [required] │ -│ --execution-url TEXT URL of execution node │ -│ --pubkeys-file-path FILE File containing the list of public keys to watch │ -│ --web3signer-url TEXT URL to web3signer managing keys to watch │ -│ --fee-recipient TEXT Fee recipient address - --execution-url must be set │ -│ --slack-channel TEXT Slack channel to send alerts - SLACK_TOKEN env var must be set │ -│ --beacon-type [lighthouse|nimbus|teku|other] Use this option if connected to a Teku < 23.6.0, Lighthouse or Nimbus beacon node. See │ -│ https://github.com/ConsenSys/teku/issues/7204 for Teku < │ -│ 23.6.0,https://github.com/sigp/lighthouse/issues/4243 for Lighthouse and │ -│ https://github.com/status-im/nimbus-eth2/issues/5019 for Nimbus. │ -│ --relay-url TEXT URL of allow listed relay │ -│ --liveness-file PATH Liveness file │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ Options ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ * --beacon-url TEXT URL of beacon node [required] │ +│ --execution-url TEXT URL of execution node │ +│ --pubkeys-file-path FILE File containing the list of public keys to watch │ +│ --web3signer-url TEXT URL to web3signer managing keys to watch │ +│ --fee-recipient TEXT Fee recipient address - --execution-url must be set │ +│ --slack-channel TEXT Slack channel to send alerts - SLACK_TOKEN env var must be set │ +│ --beacon-type [lighthouse|nimbus|prysm|teku|other] Use this option if connected to a Teku < 23.6.0, Prysm, Lighthouse or Nimbus │ +│ beacon node. See https://github.com/ConsenSys/teku/issues/7204 for Teku < 23.6.0, │ +│ https://github.com/prysmaticlabs/prysm/issues/11581 for Prysm, │ +│ https://github.com/sigp/lighthouse/issues/4243 for Lighthouse, │ +│ https://github.com/status-im/nimbus-eth2/issues/5019 and │ +│ https://github.com/status-im/nimbus-eth2/issues/5138 for Nimbus. │ +│ --relay-url TEXT URL of allow listed relay │ +│ --liveness-file PATH Liveness file │ +│ --help Show this message and exit. │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` Beacon nodes compatibility -------------------------- Beacon type | Compatibility -----------------|---------------------------------------------------------------------------------------------------------- -Prysm | Full -Teku `>= 23.6.0` | Full. You need to activate the [beacon-liveness-tracking-enabled](https://docs.teku.consensys.net/reference/cli#options) flag. -Teku `< 23.6.0 ` | Full with `--beacon-type=teku`. See https://github.com/ConsenSys/teku/pull/7212 for more details. You need to activate the [beacon-liveness-tracking-enabled](https://docs.teku.consensys.net/reference/cli#options) flag. +Prysm | Partial with `--beacon-type=prysm` - Rewards computation disabled. See https://github.com/prysmaticlabs/prysm/issues/11581 for more details. +Teku `>= 23.6.0` | Full. You need to activate the [beacon-liveness-tracking-enabled](https://docs.teku.consensys.net/reference/cli#options) flag on your beacon node. +Teku `< 23.6.0 ` | Full with `--beacon-type=teku`. See https://github.com/ConsenSys/teku/pull/7212 for more details. You need to activate the [beacon-liveness-tracking-enabled](https://docs.teku.consensys.net/reference/cli#options) flag on your beacon node. Lighthouse | Full with `--beacon-type=lighthouse`. See https://github.com/sigp/lighthouse/issues/4243 for more details. -Nimbus | Partial with `--beacon-type=nimbus` - Missed attestations detection disabled. See https://github.com/status-im/nimbus-eth2/issues/5019 for more details. +Nimbus | Partial with `--beacon-type=nimbus` - Missed attestations detection and rewards computation disabled. See https://github.com/status-im/nimbus-eth2/issues/5019 and https://github.com/status-im/nimbus-eth2/issues/5138 for more details. Lodestar | Not (yet) tested. Command lines examples diff --git a/eth_validator_watcher/beacon.py b/eth_validator_watcher/beacon.py index 272b110..5eb7007 100644 --- a/eth_validator_watcher/beacon.py +++ b/eth_validator_watcher/beacon.py @@ -42,7 +42,8 @@ def __init__(self, url: str) -> None: """ self.__url = url self.__http = Session() - self.__nimbus_first_liveness_call = False + self.__first_liveness_call = True + self.__first_rewards_call = True adapter = HTTPAdapter( max_retries=Retry( @@ -178,14 +179,40 @@ def get_duty_slot_to_committee_index_to_validators_index( return result - def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: + def get_rewards( + self, beacon_type: BeaconType, epoch: int, validators_index: set[int] + ) -> Rewards: """Get rewards. Parameters: + beacon_type : Type of beacon node epoch : Epoch corresponding to the rewards to retrieve validators_index: Set of validator indexes corresponding to the rewards to retrieve """ + + # On Prysm, because of + # https://github.com/prysmaticlabs/prysm/issues/11581, + # we just assume there is no rewards at all + + # On Nimbus, because of + # https://github.com/status-im/nimbus-eth2/issues/5138, + # we just assume there is no rewards at all + + if beacon_type in {BeaconType.NIMBUS, BeaconType.PRYSM}: + if self.__first_rewards_call: + self.__first_rewards_call = False + print( + ( + "⚠️ You are using Prysm or Nimbus. Rewards will be ignored. " + "See https://github.com/prysmaticlabs/prysm/issues/11581 " + "(Prysm) & https://github.com/status-im/nimbus-eth2/issues/5138 " + "(Nimbus) for more information." + ) + ) + + return Rewards(data=Rewards.Data(ideal_rewards=[], total_rewards=[])) + response = self.__post( f"{self.__url}/eth/v1/beacon/rewards/attestations/{epoch}", json=[str(index) for index in sorted(validators_index)], @@ -202,7 +229,7 @@ def get_validators_liveness( """Get validators liveness. Parameters : - beacon_type : Type of beacon node (Teku, Lighthouse or other) + beacon_type : Type of beacon node epoch : Epoch corresponding to the validators liveness to retrieve validators_index: Set of validator indexs corresponding to the liveness to retrieve @@ -213,11 +240,11 @@ def get_validators_liveness( # we just assume that all validators are live if beacon_type == BeaconType.NIMBUS: - if not self.__nimbus_first_liveness_call: - self.__nimbus_first_liveness_call = True + if self.__first_liveness_call: + self.__first_liveness_call = False print( ( - "⚠️ You are using Nimbus. Liveness will be ignored. " + "⚠️ You are using Nimbus. Missed attestations will be ignored. " "See https://github.com/status-im/nimbus-eth2/issues/5019 for " "more information." ) @@ -227,6 +254,7 @@ def get_validators_liveness( beacon_type_to_function = { BeaconType.LIGHTHOUSE: self.__get_validators_liveness_lighthouse, + BeaconType.PRYSM: self.__get_validators_liveness_beacon_api, BeaconType.TEKU: self.__get_validators_liveness_teku, BeaconType.OTHER: self.__get_validators_liveness_beacon_api, } diff --git a/eth_validator_watcher/entrypoint.py b/eth_validator_watcher/entrypoint.py index feea6af..da9d8a1 100644 --- a/eth_validator_watcher/entrypoint.py +++ b/eth_validator_watcher/entrypoint.py @@ -99,12 +99,13 @@ def handler( BeaconType.OTHER, case_sensitive=False, help=( - "Use this option if connected to a Teku < 23.6.0, Lighthouse or Nimbus " - "beacon node. " - "See https://github.com/ConsenSys/teku/issues/7204 for Teku < 23.6.0," - "https://github.com/sigp/lighthouse/issues/4243 for Lighthouse and " - "https://github.com/status-im/nimbus-eth2/issues/5019 for Nimbus." - "" + "Use this option if connected to a Teku < 23.6.0, Prysm, Lighthouse or " + "Nimbus beacon node. " + "See https://github.com/ConsenSys/teku/issues/7204 for Teku < 23.6.0, " + "https://github.com/prysmaticlabs/prysm/issues/11581 for Prysm, " + "https://github.com/sigp/lighthouse/issues/4243 for Lighthouse, " + "https://github.com/status-im/nimbus-eth2/issues/5019 and " + "https://github.com/status-im/nimbus-eth2/issues/5138 for Nimbus." ), show_default=False, ), @@ -374,7 +375,7 @@ def _handler( ) if should_process_rewards: - process_rewards(beacon, epoch, our_active_index_to_validator) + process_rewards(beacon, beacon_type, epoch, our_active_index_to_validator) last_rewards_process_epoch = epoch process_future_blocks_proposal(beacon, our_pubkeys, slot, is_new_epoch) diff --git a/eth_validator_watcher/models.py b/eth_validator_watcher/models.py index 26a4c2d..88bd26b 100644 --- a/eth_validator_watcher/models.py +++ b/eth_validator_watcher/models.py @@ -119,6 +119,7 @@ class CoinbaseTrade(BaseModel): class BeaconType(str, Enum): LIGHTHOUSE = "lighthouse" NIMBUS = "nimbus" + PRYSM = "prysm" TEKU = "teku" OTHER = "other" diff --git a/eth_validator_watcher/rewards.py b/eth_validator_watcher/rewards.py index 93cb5cd..db63f12 100644 --- a/eth_validator_watcher/rewards.py +++ b/eth_validator_watcher/rewards.py @@ -2,7 +2,7 @@ from typing import Tuple from .beacon import Beacon -from .models import Validators +from .models import BeaconType, Validators from prometheus_client import Gauge, Counter @@ -46,12 +46,16 @@ def process_rewards( - beacon: Beacon, epoch: int, index_to_validator: dict[int, Validator] + beacon: Beacon, + beacon_type: BeaconType, + epoch: int, + index_to_validator: dict[int, Validator], ) -> None: """Process rewards for given epoch and validators Parameters: beacon (Beacon): Beacon object + beacon_type (BeaconType): Beacon type epoch (int): Epoch number index_to_validator (dict[int, Validator]): Dictionary with: key: validator index @@ -60,7 +64,11 @@ def process_rewards( if len(index_to_validator) == 0: return - data = beacon.get_rewards(epoch - 2, set(index_to_validator)).data + data = beacon.get_rewards(beacon_type, epoch - 2, set(index_to_validator)).data + + if len(data.ideal_rewards) == 0 and len(data.total_rewards) == 0: + # We probably are connected to a beacon that does not support rewards + return effective_balance_to_ideal_reward: dict[int, Reward] = { reward.effective_balance: (reward.source, reward.target, reward.head) diff --git a/tests/beacon/test_get_rewards.py b/tests/beacon/test_get_rewards.py index 4fc8671..5e2a27b 100644 --- a/tests/beacon/test_get_rewards.py +++ b/tests/beacon/test_get_rewards.py @@ -1,14 +1,23 @@ import json from pathlib import Path -from pytest import raises -from requests import Response -from requests.exceptions import RetryError from requests_mock import Mocker -from eth_validator_watcher.beacon import Beacon, NoBlockError +from eth_validator_watcher.beacon import Beacon from tests.beacon import assets -from eth_validator_watcher.models import Rewards +from eth_validator_watcher.models import BeaconType, Rewards + + +def test_get_rewards_not_supported() -> None: + beacon = Beacon("http://beacon-node:5052") + + expected = Rewards(data=Rewards.Data(ideal_rewards=[], total_rewards=[])) + + actual = beacon.get_rewards(BeaconType.PRYSM, 42, {8499, 8500}) + assert expected == actual + + actual = beacon.get_rewards(BeaconType.NIMBUS, 42, {8499, 8500}) + assert expected == actual def test_get_rewards() -> None: @@ -50,4 +59,4 @@ def match_request(request) -> bool: json=rewards_dict, ) - assert beacon.get_rewards(42, {8499, 8500}) == expected + assert beacon.get_rewards(BeaconType.LIGHTHOUSE, 42, {8499, 8500}) == expected diff --git a/tests/entrypoint/test__handler.py b/tests/entrypoint/test__handler.py index bfdc501..7fa98d8 100644 --- a/tests/entrypoint/test__handler.py +++ b/tests/entrypoint/test__handler.py @@ -261,9 +261,13 @@ def process_missed_blocks( return True def process_rewards( - beacon: Beacon, epoch: int, our_active_index_to_validator: dict[int, Validator] + beacon: Beacon, + beacon_type: BeaconType, + epoch: int, + our_active_index_to_validator: dict[int, Validator], ): assert isinstance(beacon, Beacon) + assert isinstance(beacon_type, BeaconType) assert epoch == 1 assert our_active_index_to_validator == { 0: Validator(pubkey="0xaaa", effective_balance=32000000000, slashed=False), diff --git a/tests/rewards/test_process_rewards.py b/tests/rewards/test_process_rewards.py index 8349ee3..9400778 100644 --- a/tests/rewards/test_process_rewards.py +++ b/tests/rewards/test_process_rewards.py @@ -13,19 +13,45 @@ actual_heads_count, process_rewards, ) +from eth_validator_watcher.models import BeaconType from math import isclose + Validator = Validators.DataItem.Validator def test_process_rewards_no_validator() -> None: - process_rewards("a beacon", 42, {}) # type: ignore + process_rewards(BeaconType.LIGHTHOUSE, "a beacon", 42, {}) # type: ignore + + +def test_process_rewards_empty() -> None: + class Beacon: + def get_rewards( + self, beacon_type: BeaconType, epoch: int, validators_index: set[int] + ) -> Rewards: + assert isinstance(beacon_type, BeaconType) + assert epoch == 40 + assert validators_index == {12345} + + return Rewards( + data=Rewards.Data( + ideal_rewards=[], + total_rewards=[], + ) + ) + + beacon = Beacon() + + process_rewards(beacon, BeaconType.PRYSM, 42, {12345: "a validator"}) # type: ignore def test_process_rewards_all_validators_are_ideal() -> None: class Beacon: - def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: + def get_rewards( + self, beacon_type: BeaconType, epoch: int, validators_index: set[int] + ) -> Rewards: + assert isinstance(beacon_type, BeaconType) assert epoch == 40 assert validators_index == {1, 2, 3} @@ -73,6 +99,7 @@ def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: process_rewards( beacon, # type: ignore + BeaconType.LIGHTHOUSE, 42, { 1: Validator( @@ -142,7 +169,10 @@ def test_process_rewards_some_validators_are_ideal() -> None: """ class Beacon: - def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: + def get_rewards( + self, beacon_type: BeaconType, epoch: int, validators_index: set[int] + ) -> Rewards: + assert isinstance(beacon_type, BeaconType) assert epoch == 40 assert validators_index == {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} @@ -211,6 +241,7 @@ def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: process_rewards( beacon, # type: ignore + BeaconType.LIGHTHOUSE, 42, { 1: Validator( @@ -319,7 +350,10 @@ def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: def test_process_rewards_no_validator_is_ideal() -> None: class Beacon: - def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: + def get_rewards( + self, beacon_type: BeaconType, epoch: int, validators_index: set[int] + ) -> Rewards: + assert isinstance(beacon_type, BeaconType) assert epoch == 40 assert validators_index == {1, 2, 3} @@ -367,6 +401,7 @@ def get_rewards(self, epoch: int, validators_index: set[int]) -> Rewards: process_rewards( beacon, # type: ignore + BeaconType.LIGHTHOUSE, 42, { 1: Validator(