From 4cfcc6d719b8c06bb8a435b0f163895246d98251 Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Mon, 25 Sep 2023 15:32:55 +0200 Subject: [PATCH] Print useful information when the chain is not yet started --- eth_validator_watcher/entrypoint.py | 24 +++++++++- eth_validator_watcher/utils.py | 36 +++++++++++++-- tests/entrypoint/test__handler.py | 51 ++++++++++++++++++++- tests/utils/test_convert_seconds_to_dhms.py | 8 ++++ 4 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 tests/utils/test_convert_seconds_to_dhms.py diff --git a/eth_validator_watcher/entrypoint.py b/eth_validator_watcher/entrypoint.py index dd9e4b0..c6e44da 100644 --- a/eth_validator_watcher/entrypoint.py +++ b/eth_validator_watcher/entrypoint.py @@ -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 @@ -27,12 +28,15 @@ from .slashed_validators import SlashedValidators from .suboptimal_attestations import process_suboptimal_attestations 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, Slack, + convert_seconds_to_dhms, eth1_address_0x_prefixed, get_our_pubkeys, slots, @@ -40,8 +44,9 @@ ) from .web3signer import Web3Signer -Status = Validators.DataItem.StatusEnum +print = functools.partial(print, flush=True) +Status = Validators.DataItem.StatusEnum app = typer.Typer(add_completion=False) @@ -238,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 diff --git a/eth_validator_watcher/utils.py b/eth_validator_watcher/utils.py index a9f3643..84980e5 100644 --- a/eth_validator_watcher/utils.py +++ b/eth_validator_watcher/utils.py @@ -17,6 +17,29 @@ 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", @@ -207,9 +230,7 @@ def send_message(self, message: str) -> None: def slots(genesis_time_sec: int) -> Iterator[Tuple[int, int]]: - # max(0, ...) is used to avoid negative slot number if genesis time is not yet - # reached - next_slot = max(0, int((time() - genesis_time_sec) / NB_SECOND_PER_SLOT) + 1) + next_slot = int((time() - genesis_time_sec) / NB_SECOND_PER_SLOT) + 1 try: while True: @@ -224,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}") diff --git a/tests/entrypoint/test__handler.py b/tests/entrypoint/test__handler.py index 0d147c7..58d1d2c 100644 --- a/tests/entrypoint/test__handler.py +++ b/tests/entrypoint/test__handler.py @@ -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: @@ -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 diff --git a/tests/utils/test_convert_seconds_to_dhms.py b/tests/utils/test_convert_seconds_to_dhms.py new file mode 100644 index 0000000..c79c1c6 --- /dev/null +++ b/tests/utils/test_convert_seconds_to_dhms.py @@ -0,0 +1,8 @@ +from eth_validator_watcher.utils import convert_seconds_to_dhms + + +def test_convert_secondes_to_dhms() -> None: + assert convert_seconds_to_dhms(0) == (0, 0, 0, 0) + assert convert_seconds_to_dhms(61) == (0, 0, 1, 1) + assert convert_seconds_to_dhms(3601) == (0, 1, 0, 1) + assert convert_seconds_to_dhms(86462) == (1, 0, 1, 2)