Skip to content

Commit

Permalink
Rewards: Add correct Prysm/Nimbus (absence of) support.
Browse files Browse the repository at this point in the history
  • Loading branch information
nalepae committed Jul 25, 2023
1 parent 2da3aaa commit 08603a8
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 46 deletions.
40 changes: 21 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 34 additions & 6 deletions eth_validator_watcher/beacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)],
Expand All @@ -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
Expand All @@ -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."
)
Expand All @@ -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,
}
Expand Down
15 changes: 8 additions & 7 deletions eth_validator_watcher/entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions eth_validator_watcher/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class CoinbaseTrade(BaseModel):
class BeaconType(str, Enum):
LIGHTHOUSE = "lighthouse"
NIMBUS = "nimbus"
PRYSM = "prysm"
TEKU = "teku"
OTHER = "other"

Expand Down
14 changes: 11 additions & 3 deletions eth_validator_watcher/rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
21 changes: 15 additions & 6 deletions tests/beacon/test_get_rewards.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
6 changes: 5 additions & 1 deletion tests/entrypoint/test__handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
43 changes: 39 additions & 4 deletions tests/rewards/test_process_rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 08603a8

Please sign in to comment.