From 125dc749f1edecefea75e52d2ff043d74a34f18f Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Fri, 15 Sep 2023 11:58:19 +0200 Subject: [PATCH 1/5] Implement networks with genesis not yet reached --- eth_validator_watcher/missed_attestations.py | 6 ++++++ eth_validator_watcher/rewards.py | 16 +++++--------- .../suboptimal_attestations.py | 3 +++ eth_validator_watcher/utils.py | 4 +++- ...test_process_double_missed_attestations.py | 15 +++++++++++++ .../test_process_missed_attestations.py | 16 ++++++++++++++ tests/rewards/test_process_rewards_net.py | 13 ++++++++++++ .../test_suboptimal_attestations.py | 21 ++++++++++++++++--- 8 files changed, 79 insertions(+), 15 deletions(-) diff --git a/eth_validator_watcher/missed_attestations.py b/eth_validator_watcher/missed_attestations.py index 75b88f6..a84208d 100644 --- a/eth_validator_watcher/missed_attestations.py +++ b/eth_validator_watcher/missed_attestations.py @@ -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 @@ -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)) diff --git a/eth_validator_watcher/rewards.py b/eth_validator_watcher/rewards.py index 3f6ea17..1bb9d73 100644 --- a/eth_validator_watcher/rewards.py +++ b/eth_validator_watcher/rewards.py @@ -26,11 +26,7 @@ Gauge("net_suboptimal_heads_rate", "Network suboptimal heads rate"), ) -( - net_ideal_sources_count, - net_ideal_targets_count, - net_ideal_heads_count, -) = ( +(net_ideal_sources_count, net_ideal_targets_count, net_ideal_heads_count,) = ( Counter("net_ideal_sources_count", "Network ideal sources count"), Counter("net_ideal_targets_count", "Network ideal targets count"), Counter("net_ideal_heads_count", "Network ideal heads count"), @@ -62,11 +58,7 @@ Gauge("our_suboptimal_heads_rate", "Our suboptimal heads rate"), ) -( - our_ideal_sources_count, - our_ideal_targets_count, - our_ideal_heads_count, -) = ( +(our_ideal_sources_count, our_ideal_targets_count, our_ideal_heads_count,) = ( Counter("our_ideal_sources_count", "Our ideal sources count"), Counter("our_ideal_targets_count", "Our ideal targets count"), Counter("our_ideal_heads_count", "Our ideal heads count"), @@ -140,9 +132,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 = ( diff --git a/eth_validator_watcher/suboptimal_attestations.py b/eth_validator_watcher/suboptimal_attestations.py index 8ac4664..b930210 100644 --- a/eth_validator_watcher/suboptimal_attestations.py +++ b/eth_validator_watcher/suboptimal_attestations.py @@ -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 diff --git a/eth_validator_watcher/utils.py b/eth_validator_watcher/utils.py index 3f5ba89..6e6f70d 100644 --- a/eth_validator_watcher/utils.py +++ b/eth_validator_watcher/utils.py @@ -207,7 +207,9 @@ def send_message(self, message: str) -> None: def slots(genesis_time_sec: int) -> Iterator[Tuple[int, int]]: - next_slot = int((time() - genesis_time_sec) / NB_SECOND_PER_SLOT) + 1 + # 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) try: while True: diff --git a/tests/missed_attestations/test_process_double_missed_attestations.py b/tests/missed_attestations/test_process_double_missed_attestations.py index 4794669..307c8c3 100644 --- a/tests/missed_attestations/test_process_double_missed_attestations.py +++ b/tests/missed_attestations/test_process_double_missed_attestations.py @@ -7,6 +7,21 @@ Validator = Validators.DataItem.Validator +def test_process_double_missed_attestations_low_epoch() -> None: + for epoch in 0, 1: + actual = process_double_missed_attestations( + {42, 43, 44, 45}, + {40, 41, 42, 43}, + LimitedDict(0), + epoch, + None, + ) + + expected: set[int] = set() + + assert expected == actual + + def test_process_double_missed_attestations_some_dead_indexes() -> None: class Slack: def __init__(self): diff --git a/tests/missed_attestations/test_process_missed_attestations.py b/tests/missed_attestations/test_process_missed_attestations.py index 0c9ad94..4328880 100644 --- a/tests/missed_attestations/test_process_missed_attestations.py +++ b/tests/missed_attestations/test_process_missed_attestations.py @@ -7,6 +7,22 @@ Validator = Validators.DataItem.Validator +def test_process_missed_attestations_low_epoch() -> None: + class Beacon: + pass + + actual = process_missed_attestations( + beacon=Beacon(), # type: ignore + beacon_type=BeaconType.OLD_TEKU, + epoch_to_index_to_validator_index=LimitedDict(0), + epoch=0, + ) + + expected: set[int] = set() + + assert expected == actual + + def test_process_missed_attestations_some_dead_indexes() -> None: class Beacon: @staticmethod diff --git a/tests/rewards/test_process_rewards_net.py b/tests/rewards/test_process_rewards_net.py index 7a019d0..0b6004d 100644 --- a/tests/rewards/test_process_rewards_net.py +++ b/tests/rewards/test_process_rewards_net.py @@ -21,6 +21,19 @@ Validator = Validators.DataItem.Validator +def test_process_rewards_low_epoch() -> None: + for epoch in 0, 1: + # We just want to be sure it doesn't raise an exception. + + process_rewards( + BeaconType.LIGHTHOUSE, # type: ignore + "a beacon", # type: ignore + epoch, + LimitedDict(2), + LimitedDict(2), + ) + + def test_process_rewards_no_network_validator() -> None: net_epoch_to_index_to_validator = LimitedDict(2) net_epoch_to_index_to_validator[42] = {} diff --git a/tests/suboptimal_attestations/test_suboptimal_attestations.py b/tests/suboptimal_attestations/test_suboptimal_attestations.py index b3ac73a..f4f8657 100644 --- a/tests/suboptimal_attestations/test_suboptimal_attestations.py +++ b/tests/suboptimal_attestations/test_suboptimal_attestations.py @@ -58,7 +58,20 @@ def aggregate_attestations( } -def test_our_pubkeys(): +def test_low_slot() -> None: + expected: set[int] = set() + + actual = process_suboptimal_attestations( + beacon="A dummy beacon", # type: ignore + block="A dummy block", # type: ignore + slot=0, + our_active_validators_index_to_validator={}, + ) + + assert expected == actual + + +def test_our_pubkeys() -> None: """ In this test case, our pubkeys are "0xaaa" ==> "0xggg". Only "0xaaa", "0xccc", "0xeee" & "0xggg" are active. @@ -122,7 +135,8 @@ def get_duty_slot_to_committee_index_to_validators_index( suboptimal_attestations.aggregate_attestations = aggregate_attestations - assert process_suboptimal_attestations( + expected = {10, 70} + actual = process_suboptimal_attestations( beacon=Beacon(), # type: ignore block="A dummy block", # type: ignore slot=42, @@ -132,6 +146,7 @@ def get_duty_slot_to_committee_index_to_validators_index( 50: Validator(pubkey="0xeee", effective_balance=32000000000, slashed=False), 70: Validator(pubkey="0xggg", effective_balance=32000000000, slashed=False), }, - ) == {10, 70} + ) + assert expected == actual assert suboptimal_attestations_rate_gauge.collect()[0].samples[0].value == 50.0 # type: ignore From 2faaba39cd684b400342d01bd26c72ba0033c2ae Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Sat, 23 Sep 2023 00:08:54 +0200 Subject: [PATCH 2/5] `MISSED_BLOCK_TIMEOUT_SET`: `9` ==> `10` --- eth_validator_watcher/entrypoint.py | 12 +++++------- eth_validator_watcher/rewards.py | 12 ++++++++++-- eth_validator_watcher/utils.py | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/eth_validator_watcher/entrypoint.py b/eth_validator_watcher/entrypoint.py index b9e1335..dd9e4b0 100644 --- a/eth_validator_watcher/entrypoint.py +++ b/eth_validator_watcher/entrypoint.py @@ -22,26 +22,24 @@ 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, + MISSED_BLOCK_TIMEOUT_SEC, NB_SLOT_PER_EPOCH, SLOT_FOR_MISSED_ATTESTATIONS_PROCESS, SLOT_FOR_REWARDS_PROCESS, LimitedDict, Slack, + 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 - Status = Validators.DataItem.StatusEnum @@ -364,7 +362,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) diff --git a/eth_validator_watcher/rewards.py b/eth_validator_watcher/rewards.py index 1bb9d73..f130963 100644 --- a/eth_validator_watcher/rewards.py +++ b/eth_validator_watcher/rewards.py @@ -26,7 +26,11 @@ Gauge("net_suboptimal_heads_rate", "Network suboptimal heads rate"), ) -(net_ideal_sources_count, net_ideal_targets_count, net_ideal_heads_count,) = ( +( + net_ideal_sources_count, + net_ideal_targets_count, + net_ideal_heads_count, +) = ( Counter("net_ideal_sources_count", "Network ideal sources count"), Counter("net_ideal_targets_count", "Network ideal targets count"), Counter("net_ideal_heads_count", "Network ideal heads count"), @@ -58,7 +62,11 @@ Gauge("our_suboptimal_heads_rate", "Our suboptimal heads rate"), ) -(our_ideal_sources_count, our_ideal_targets_count, our_ideal_heads_count,) = ( +( + our_ideal_sources_count, + our_ideal_targets_count, + our_ideal_heads_count, +) = ( Counter("our_ideal_sources_count", "Our ideal sources count"), Counter("our_ideal_targets_count", "Our ideal targets count"), Counter("our_ideal_heads_count", "Our ideal heads count"), diff --git a/eth_validator_watcher/utils.py b/eth_validator_watcher/utils.py index 6e6f70d..a9f3643 100644 --- a/eth_validator_watcher/utils.py +++ b/eth_validator_watcher/utils.py @@ -11,7 +11,7 @@ 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 From 2322f5fe67d8005ca2710096761094c3f5c621f0 Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Sat, 23 Sep 2023 19:38:35 +0200 Subject: [PATCH 3/5] Add some more entries to `BUCKET` --- eth_validator_watcher/entry_queue.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/eth_validator_watcher/entry_queue.py b/eth_validator_watcher/entry_queue.py index 6dae6bf..79f4236 100644 --- a/eth_validator_watcher/entry_queue.py +++ b/eth_validator_watcher/entry_queue.py @@ -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), @@ -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( From aaff6c738c2fadf5c5b5501533bdb6cd51c5a897 Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Sat, 23 Sep 2023 19:39:40 +0200 Subject: [PATCH 4/5] Entry queue: Remove pessimistic estimation --- eth_validator_watcher/entry_queue.py | 39 +++++-------------- .../entry_queue/test_compute_duration_sec.py | 13 +++++++ .../test_compute_optimistic_duration_sec.py | 16 -------- .../test_compute_pessimistic_duration_sec.py | 14 ------- tests/entry_queue/test_get_bucket_index.py | 2 +- 5 files changed, 24 insertions(+), 60 deletions(-) create mode 100644 tests/entry_queue/test_compute_duration_sec.py delete mode 100644 tests/entry_queue/test_compute_optimistic_duration_sec.py delete mode 100644 tests/entry_queue/test_compute_pessimistic_duration_sec.py diff --git a/eth_validator_watcher/entry_queue.py b/eth_validator_watcher/entry_queue.py index 79f4236..6ff6d72 100644 --- a/eth_validator_watcher/entry_queue.py +++ b/eth_validator_watcher/entry_queue.py @@ -64,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. @@ -91,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 @@ -104,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 @@ -116,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: @@ -153,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) diff --git a/tests/entry_queue/test_compute_duration_sec.py b/tests/entry_queue/test_compute_duration_sec.py new file mode 100644 index 0000000..940cac0 --- /dev/null +++ b/tests/entry_queue/test_compute_duration_sec.py @@ -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 diff --git a/tests/entry_queue/test_compute_optimistic_duration_sec.py b/tests/entry_queue/test_compute_optimistic_duration_sec.py deleted file mode 100644 index 01fa206..0000000 --- a/tests/entry_queue/test_compute_optimistic_duration_sec.py +++ /dev/null @@ -1,16 +0,0 @@ -from eth_validator_watcher.entry_queue import ( - NB_SECONDS_PER_EPOCH, - compute_optimistic_duration_sec, -) - - -def test_compute_optimistic_duration_sec_buckets_differ() -> None: - assert ( - compute_optimistic_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_optimistic_duration_sec_buckets_same() -> None: - assert compute_optimistic_duration_sec(4, 9) == 2 * NB_SECONDS_PER_EPOCH diff --git a/tests/entry_queue/test_compute_pessimistic_duration_sec.py b/tests/entry_queue/test_compute_pessimistic_duration_sec.py deleted file mode 100644 index f496e5f..0000000 --- a/tests/entry_queue/test_compute_pessimistic_duration_sec.py +++ /dev/null @@ -1,14 +0,0 @@ -from eth_validator_watcher.entry_queue import ( - NB_SECONDS_PER_EPOCH, compute_pessimistic_duration_sec) - - -def test_compute_pessimistic_duration_sec() -> None: - assert compute_pessimistic_duration_sec(42_000, 0) == 0 - assert compute_pessimistic_duration_sec(42_000, 3) == 0 - assert compute_pessimistic_duration_sec(42_000, 4) == NB_SECONDS_PER_EPOCH - assert compute_pessimistic_duration_sec(42_000, 5) == NB_SECONDS_PER_EPOCH - - assert ( - compute_pessimistic_duration_sec(678_000, 100_000) - == (100_000 // 10) * NB_SECONDS_PER_EPOCH - ) diff --git a/tests/entry_queue/test_get_bucket_index.py b/tests/entry_queue/test_get_bucket_index.py index 23d6483..b7efa6c 100644 --- a/tests/entry_queue/test_get_bucket_index.py +++ b/tests/entry_queue/test_get_bucket_index.py @@ -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) From bd2000e9ec05ae6aee79ca07c35a4197feef77ce Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Mon, 25 Sep 2023 15:32:55 +0200 Subject: [PATCH 5/5] 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)