Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement non-reached genesis networks #70

Merged
merged 5 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 31 additions & 30 deletions eth_validator_watcher/entry_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
NB_SECONDS_PER_EPOCH = NB_SECONDS_PER_SLOT * NB_SLOT_PER_EPOCH

# TODO: Compute this dynamically
BUCKETS = [
BUCKETS: list[tuple[int, int]] = [
(0, 4),
(327_680, 5),
(393_216, 6),
Expand All @@ -27,6 +27,26 @@
(1_179_648, 18),
(1_245_184, 19),
(1_310_720, 20),
(1_376_256, 21),
(1_441_792, 22),
(1_507_328, 23),
(1_572_864, 24),
(1_638_400, 25),
(1_703_936, 26),
(1_769_472, 27),
(1_835_008, 28),
(1_900_544, 29),
(1_966_080, 30),
(2_031_616, 31),
(2_097_152, 32),
(2_162_688, 33),
(2_228_224, 34),
(2_293_760, 35),
(2_359_296, 36),
(2_424_832, 37),
(2_490_368, 38),
(2_555_904, 39),
(2_621_440, 40),
]

entry_queue_duration_sec = Gauge(
Expand All @@ -44,20 +64,6 @@ def compute_validators_churn(nb_active_validators: int) -> int:
return max(MIN_PER_EPOCH_CHURN_LIMIT, nb_active_validators // CHURN_LIMIT_QUOTIENT)


def compute_pessimistic_duration_sec(
nb_active_validators: int, position_in_entry_queue: int
) -> int:
"""Compute a pessimistic estimation of when a validator will exit the entry queue.

Parameters:
nb_active_validators: The number of currently active validators
position_in_entry_queue: The position of the validator in the entry queue
"""
return (
position_in_entry_queue // compute_validators_churn(nb_active_validators)
) * NB_SECONDS_PER_EPOCH


def get_bucket_index(validator_index: int) -> int:
"""Get the bucket index of a validator.

Expand All @@ -71,10 +77,11 @@ def get_bucket_index(validator_index: int) -> int:
raise RuntimeError("Validator index is too high")


def compute_optimistic_duration_sec(
def compute_duration_sec(
nb_active_validators: int, position_in_entry_queue: int
) -> int:
"""Compute an optimistic estimation of when a validator will exit the entry queue.
"""Compute the remaining time before a validator is active if no validator wants to
exit.

Parameters:
nb_active_validators : The number of currently active validators
Expand All @@ -84,10 +91,9 @@ def compute_optimistic_duration_sec(
stop_bucket_index = get_bucket_index(nb_active_validators + position_in_entry_queue)

if start_bucket_index == stop_bucket_index:
return compute_pessimistic_duration_sec(
nb_active_validators, position_in_entry_queue
)

return (
position_in_entry_queue // compute_validators_churn(nb_active_validators)
) * NB_SECONDS_PER_EPOCH
# Compute the number of validators in the first bucket
start_limit, _ = BUCKETS[start_bucket_index + 1]
number_validators_in_start_bucket = start_limit - nb_active_validators
Expand All @@ -96,7 +102,7 @@ def compute_optimistic_duration_sec(
stop_limit, _ = BUCKETS[stop_bucket_index]

number_validators_in_stop_bucket = (
nb_active_validators + position_in_entry_queue - stop_limit + 1
nb_active_validators + position_in_entry_queue + 1 - stop_limit
)

def fill_bucket(index: int) -> int:
Expand Down Expand Up @@ -133,11 +139,6 @@ def export_duration_sec(
nb_active_validators : The number of currently active validators
position_in_entry_queue: The position of the validator in the entry queue
"""
result = (
compute_optimistic_duration_sec(nb_active_validators, position_in_entry_queue)
+ compute_pessimistic_duration_sec(
nb_active_validators, position_in_entry_queue
)
) // 2

entry_queue_duration_sec.set(result)

duration_sec = compute_duration_sec(nb_active_validators, position_in_entry_queue)
entry_queue_duration_sec.set(duration_sec)
34 changes: 27 additions & 7 deletions eth_validator_watcher/entrypoint.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Entrypoint for the eth-validator-watcher CLI."""

import functools
from os import environ
from pathlib import Path
from time import sleep, time
Expand All @@ -22,29 +23,31 @@
from .missed_blocks import process_missed_blocks
from .models import BeaconType, Validators
from .next_blocks_proposal import process_future_blocks_proposal
from .relays import Relays
from .rewards import process_rewards
from .slashed_validators import SlashedValidators
from .suboptimal_attestations import process_suboptimal_attestations
from .utils import (
BLOCK_NOT_ORPHANED_TIME_SEC,
CHUCK_NORRIS,
MISSED_BLOCK_TIMEOUT_SEC,
NB_SECOND_PER_SLOT,
NB_SLOT_PER_EPOCH,
SLOT_FOR_MISSED_ATTESTATIONS_PROCESS,
SLOT_FOR_REWARDS_PROCESS,
LimitedDict,
Slack,
convert_seconds_to_dhms,
eth1_address_0x_prefixed,
get_our_pubkeys,
slots,
write_liveness_file,
eth1_address_0x_prefixed,
)

from .rewards import process_rewards
from .web3signer import Web3Signer

from .relays import Relays
print = functools.partial(print, flush=True)

Status = Validators.DataItem.StatusEnum


app = typer.Typer(add_completion=False)

slot_gauge = Gauge("slot", "Slot")
Expand Down Expand Up @@ -240,6 +243,23 @@ def _handler(
genesis = beacon.get_genesis()

for slot, slot_start_time_sec in slots(genesis.data.genesis_time):
if slot < 0:
chain_start_in_sec = -slot * NB_SECOND_PER_SLOT
days, hours, minutes, seconds = convert_seconds_to_dhms(chain_start_in_sec)

print(
f"⏱️ The chain will start in {days:2} days, {hours:2} hours, "
f"{minutes:2} minutes and {seconds:2} seconds."
)

if slot % NB_SLOT_PER_EPOCH == 0:
print(f"💪 {CHUCK_NORRIS[slot%len(CHUCK_NORRIS)]}")

if liveness_file is not None:
write_liveness_file(liveness_file)

continue

epoch = slot // NB_SLOT_PER_EPOCH
slot_in_epoch = slot % NB_SLOT_PER_EPOCH

Expand Down Expand Up @@ -364,7 +384,7 @@ def _handler(

process_future_blocks_proposal(beacon, our_pubkeys, slot, is_new_epoch)

delta_sec = BLOCK_NOT_ORPHANED_TIME_SEC - (time() - slot_start_time_sec)
delta_sec = MISSED_BLOCK_TIMEOUT_SEC - (time() - slot_start_time_sec)
sleep(max(0, delta_sec))

potential_block = beacon.get_potential_block(slot)
Expand Down
6 changes: 6 additions & 0 deletions eth_validator_watcher/missed_attestations.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def process_missed_attestations(
inner value : validators
epoch : Epoch where the missed attestations are checked
"""
if epoch < 1:
return set()

index_to_validator: dict[int, Validators.DataItem.Validator] = (
epoch_to_index_to_validator_index[epoch - 1]
if epoch - 1 in epoch_to_index_to_validator_index
Expand Down Expand Up @@ -102,6 +105,9 @@ def process_double_missed_attestations(
epoch : Epoch where the missed attestations are checked
slack : Slack instance
"""
if epoch < 2:
return set()

double_dead_indexes = dead_indexes & previous_dead_indexes
double_missed_attestations_count.set(len(double_dead_indexes))

Expand Down
4 changes: 3 additions & 1 deletion eth_validator_watcher/rewards.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,11 @@ def process_rewards(
outer key : epoch
outer value, inner key: validator indexes
inner value : validators

"""

if epoch < 2:
return

# Network validators
# ------------------
net_index_to_validator = (
Expand Down
3 changes: 3 additions & 0 deletions eth_validator_watcher/suboptimal_attestations.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def process_suboptimal_attestations(
- key : index of our active validator
- value: public key of our active validator
"""
if slot < 1:
return set()

previous_slot = slot - 1

# Epoch of previous slot is NOT the previous epoch, but really the epoch
Expand Down
34 changes: 33 additions & 1 deletion eth_validator_watcher/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,35 @@

NB_SLOT_PER_EPOCH = 32
NB_SECOND_PER_SLOT = 12
BLOCK_NOT_ORPHANED_TIME_SEC = 9
MISSED_BLOCK_TIMEOUT_SEC = 10
SLOT_FOR_MISSED_ATTESTATIONS_PROCESS = 16
SLOT_FOR_REWARDS_PROCESS = 17
ETH1_ADDRESS_LEN = 40
ETH2_ADDRESS_LEN = 96

CHUCK_NORRIS = [
"Chuck Norris doesn't stake Ethers; he stares at the blockchain, and it instantly "
"produces new coins.",
"When Chuck Norris sends Ethers, it doesn't need confirmations. The Ethereum "
"network just knows better than to mess with Chuck.",
"Chuck Norris once hacked into a smart contract without using a computer. He just "
"stared at the code, and it fixed itself.",
"Ethereum's gas fees are afraid of Chuck Norris. They lower themselves just to "
"avoid his wrath.",
"Chuck Norris doesn't need a private key to access his Ethereum wallet. He just "
"flexes his biceps, and it opens.",
"When Chuck Norris trades on a decentralized exchange, the price slippage goes in "
"his favor, no matter what.",
"Vitalik Buterin once challenged Chuck Norris to a coding contest. Chuck won by "
"writing Ethereum's whitepaper with his eyes closed.",
"Chuck Norris's Ethereum nodes are so fast that they can process transactions "
"before they even happen.",
'The Ethereum community calls Chuck Norris the "Smart Contract Whisperer" '
"because he can make any contract do his bidding.",
"When Chuck Norris checks his Ethereum balance, the wallet interface just says, "
'"Infinite."',
]

keys_count = Gauge(
"keys_count",
"Keys count",
Expand Down Expand Up @@ -222,6 +245,15 @@ def slots(genesis_time_sec: int) -> Iterator[Tuple[int, int]]:
pass # pragma: no cover


def convert_seconds_to_dhms(seconds: int) -> tuple[int, int, int, int]:
# Calculate days, hours, minutes, and seconds
days, seconds = divmod(seconds, 86400) # 1 day = 24 hours * 60 minutes * 60 seconds
hours, seconds = divmod(seconds, 3600) # 1 hour = 60 minutes * 60 seconds
minutes, seconds = divmod(seconds, 60) # 1 minute = 60 seconds

return days, hours, minutes, seconds


def eth1_address_0x_prefixed(address: str) -> str:
if not re.match(f"^(0x)?[0-9a-fA-F]{{{ETH1_ADDRESS_LEN}}}$", address):
raise ValueError(f"Invalid ETH1 address: {address}")
Expand Down
13 changes: 13 additions & 0 deletions tests/entry_queue/test_compute_duration_sec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from eth_validator_watcher.entry_queue import NB_SECONDS_PER_EPOCH, compute_duration_sec


def test_compute_duration_sec_buckets_differ() -> None:
assert (
compute_duration_sec(327_678, 589_826 - 327_678)
== (2 // 4 + 65536 // 5 + 65536 // 6 + 65536 // 7 + 65536 // 8 + 3 // 9)
* NB_SECONDS_PER_EPOCH
)


def test_compute_duration_sec_buckets_same() -> None:
assert compute_duration_sec(4, 9) == 2 * NB_SECONDS_PER_EPOCH
16 changes: 0 additions & 16 deletions tests/entry_queue/test_compute_optimistic_duration_sec.py

This file was deleted.

14 changes: 0 additions & 14 deletions tests/entry_queue/test_compute_pessimistic_duration_sec.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/entry_queue/test_get_bucket_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ def test_get_bucket_index_nominal() -> None:

def test_get_bucket_index_raise() -> None:
with raises(RuntimeError):
get_bucket_index(1_310_724)
get_bucket_index(3_000_000)
51 changes: 50 additions & 1 deletion tests/entrypoint/test__handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,55 @@ def start_http_server(_: int) -> None:
)


def test_invalid_chain_not_ready() -> None:
class Beacon:
def __init__(self, url: str) -> None:
assert url == "http://localhost:5052"

def get_genesis(self) -> Genesis:
return Genesis(
data=Genesis.Data(
genesis_time=0,
)
)

def get_our_pubkeys(pubkeys_file_path: Path, web3signer: None) -> set[str]:
return {"0x12345", "0x67890"}

def slots(genesis_time: int) -> Iterator[Tuple[(int, int)]]:
assert genesis_time == 0
yield -32, 1664

def convert_seconds_to_dhms(seconds: int) -> Tuple[int, int, int, int]:
assert seconds == 384
return 42, 42, 42, 42

def write_liveness_file(liveness_file: Path) -> None:
assert liveness_file == Path("/path/to/liveness")

def start_http_server(_: int) -> None:
pass

entrypoint.get_our_pubkeys = get_our_pubkeys
entrypoint.Beacon = Beacon
entrypoint.slots = slots
entrypoint.convert_seconds_to_dhms = convert_seconds_to_dhms
entrypoint.write_liveness_file = write_liveness_file
entrypoint.start_http_server = start_http_server

_handler(
beacon_url="http://localhost:5052",
execution_url=None,
pubkeys_file_path=Path("/path/to/pubkeys"),
web3signer_url=None,
fee_recipient=None,
slack_channel=None,
beacon_type=BeaconType.OLD_TEKU,
relays_url=[],
liveness_file=Path("/path/to/liveness"),
)


@freeze_time("2023-01-01 00:00:00", auto_tick_seconds=15)
def test_nominal() -> None:
class Beacon:
Expand Down Expand Up @@ -263,7 +312,7 @@ def process_rewards(
epoch: int,
net_epoch2active_idx2val: dict[int, Validator],
our_epoch2active_idx2val: dict[int, Validator],
):
) -> None:
assert isinstance(beacon, Beacon)
assert isinstance(beacon_type, BeaconType)
assert epoch == 1
Expand Down
Loading