diff --git a/eth_validator_watcher/entrypoint.py b/eth_validator_watcher/entrypoint.py index bc389d4..d5767df 100644 --- a/eth_validator_watcher/entrypoint.py +++ b/eth_validator_watcher/entrypoint.py @@ -30,9 +30,9 @@ SLOT_FOR_MISSED_ATTESTATIONS_PROCESS, Slack, get_our_pubkeys, - is_eth1_address, slots, write_liveness_file, + eth1_address_0x_prefixed, ) from .web3signer import Web3Signer @@ -182,10 +182,11 @@ def _handler( "`execution-url` must be set if you want to use `fee-recipient`" ) - if fee_recipient is not None and not is_eth1_address(fee_recipient): - raise typer.BadParameter( - "`fee-recipient` should be a valid, 0x prefixed, eth1 address" - ) + if fee_recipient is not None: + try: + fee_recipient = eth1_address_0x_prefixed(fee_recipient) + except ValueError: + raise typer.BadParameter("`fee-recipient` should be a valid ETH1 address") if slack_channel is not None and slack_token is None: raise typer.BadParameter( @@ -229,7 +230,11 @@ def _handler( is_new_epoch = previous_epoch is None or previous_epoch != epoch if is_new_epoch: - our_pubkeys = get_our_pubkeys(pubkeys_file_path, web3signer) + try: + our_pubkeys = get_our_pubkeys(pubkeys_file_path, web3signer) + except ValueError: + raise typer.BadParameter("Some pubkeys are invalid") + total_status_to_index_to_validator = ( beacon.get_status_to_index_to_validator() ) diff --git a/eth_validator_watcher/utils.py b/eth_validator_watcher/utils.py index 54899ae..95a3501 100644 --- a/eth_validator_watcher/utils.py +++ b/eth_validator_watcher/utils.py @@ -6,12 +6,16 @@ from prometheus_client import Gauge from slack_sdk import WebClient +import re + from .web3signer import Web3Signer NB_SLOT_PER_EPOCH = 32 NB_SECOND_PER_SLOT = 12 BLOCK_NOT_ORPHANED_TIME_SEC = 9 SLOT_FOR_MISSED_ATTESTATIONS_PROCESS = 16 +ETH1_ADDRESS_LEN = 40 +ETH2_ADDRESS_LEN = 96 keys_count = Gauge( "keys_count", @@ -151,7 +155,7 @@ def load_pubkeys_from_file(path: Path) -> set[str]: Returns the corresponding set of public keys. """ with path.open() as file_descriptor: - return set((f"0x{line.strip()}" for line in file_descriptor)) + return set((eth2_address_0x_prefixed(line.strip()) for line in file_descriptor)) def get_our_pubkeys( @@ -218,5 +222,21 @@ def slots(genesis_time_sec: int) -> Iterator[Tuple[int, int]]: pass # pragma: no cover -def is_eth1_address(address: str) -> bool: - return address[:2] == "0x" and len(address) == 42 +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}") + + if len(address) == ETH1_ADDRESS_LEN: + return f"0x{address}" + + return address + + +def eth2_address_0x_prefixed(address: str) -> str: + if not re.match(f"^(0x)?[0-9a-fA-F]{{{ETH2_ADDRESS_LEN}}}$", address): + raise ValueError(f"Invalid ETH2 address: {address}") + + if len(address) == ETH2_ADDRESS_LEN: + return f"0x{address}" + + return address diff --git a/tests/entrypoint/test__handler.py b/tests/entrypoint/test__handler.py index d30a16d..9a4a4ac 100644 --- a/tests/entrypoint/test__handler.py +++ b/tests/entrypoint/test__handler.py @@ -57,6 +57,50 @@ def test_slack_token_not_defined() -> None: ) +def test_invalid_pubkeys() -> 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, + genesis_fork_version="0x123", + genesis_validators_root="0xabc", + ) + ) + + def get_our_pubkeys(pubkeys_file_path: Path, web3signer: None) -> set[str]: + assert pubkeys_file_path == Path("/path/to/pubkeys") + raise ValueError("Invalid pubkeys") + + def slots(genesis_time: int) -> Iterator[Tuple[(int, int)]]: + assert genesis_time == 0 + yield 63, 1664 + yield 64, 1676 + + def start_http_server(_: int) -> None: + pass + + entrypoint.get_our_pubkeys = get_our_pubkeys # type: ignore + entrypoint.Beacon = Beacon # type: ignore + entrypoint.slots = slots # type: ignore + entrypoint.start_http_server = start_http_server # type: ignore + + with raises(BadParameter): + _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.TEKU, + liveness_file=None, + ) + + @freeze_time("2023-01-01 00:00:00", auto_tick_seconds=15) def test_nominal() -> None: class Beacon: diff --git a/tests/utils/assets/pubkeys.txt b/tests/utils/assets/pubkeys.txt index 1802a74..590e201 100644 --- a/tests/utils/assets/pubkeys.txt +++ b/tests/utils/assets/pubkeys.txt @@ -1,3 +1,3 @@ -aaa -bbb -ccc +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc diff --git a/tests/utils/test_eth1_address_0x_prefixed.py b/tests/utils/test_eth1_address_0x_prefixed.py new file mode 100644 index 0000000..5d526e2 --- /dev/null +++ b/tests/utils/test_eth1_address_0x_prefixed.py @@ -0,0 +1,29 @@ +from eth_validator_watcher.utils import eth1_address_0x_prefixed +from pytest import raises + + +def test_eth1_address_0x_prefixed_invalid() -> None: + # Too short + with raises(ValueError): + eth1_address_0x_prefixed("0x123") + + # Too long + with raises(ValueError): + eth1_address_0x_prefixed( + "0x123456789012345678901234567890123456789012345678901234567890123" + ) + + # Invalid character + with raises(ValueError): + eth1_address_0x_prefixed("0x8d8b1b85d02d05ad3a14e2e9cc7b458d5invalid") + + +def test_eth1_address_0x_prefixed_valid_already_prefixed() -> None: + address = "0x8d8b1b85d02d05ad3a14e2e9cc7b458d5c7d8f8c" + assert eth1_address_0x_prefixed(address) == address + + +def test_eth1_address_0x_prefixed_valid_not_already_prefixed() -> None: + address_without_prefix = "8d8b1b85d02d05ad3a14e2e9cc7b458d5c7d8f8c" + address_with_prefix = "0x8d8b1b85d02d05ad3a14e2e9cc7b458d5c7d8f8c" + assert eth1_address_0x_prefixed(address_without_prefix) == address_with_prefix diff --git a/tests/utils/test_eth2_address_0x_prefixed.py b/tests/utils/test_eth2_address_0x_prefixed.py new file mode 100644 index 0000000..f32a65f --- /dev/null +++ b/tests/utils/test_eth2_address_0x_prefixed.py @@ -0,0 +1,29 @@ +from eth_validator_watcher.utils import eth2_address_0x_prefixed +from pytest import raises + + +def test_eth2_address_0x_prefixed_invalid() -> None: + # Too short + with raises(ValueError): + eth2_address_0x_prefixed("0x123") + + # Too long + with raises(ValueError): + eth2_address_0x_prefixed( + "0x123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" + ) + + # Invalid character + with raises(ValueError): + eth2_address_0x_prefixed("0x8d8b1b85d02d05ad3a14e2e9cc7b458d5invalid") + + +def test_eth2_address_0x_prefixed_valid_already_prefixed() -> None: + address = "0xb2ddae7e32fd8257c2dd468ca16dc86d310cab218f9a41ed6fabea525a9620d46955350776e8496553138c8a291a365b" + assert eth2_address_0x_prefixed(address) == address + + +def test_eth2_address_0x_prefixed_valid_not_already_prefixed() -> None: + address_without_prefix = "b2ddae7e32fd8257c2dd468ca16dc86d310cab218f9a41ed6fabea525a9620d46955350776e8496553138c8a291a365b" + address_with_prefix = "0xb2ddae7e32fd8257c2dd468ca16dc86d310cab218f9a41ed6fabea525a9620d46955350776e8496553138c8a291a365b" + assert eth2_address_0x_prefixed(address_without_prefix) == address_with_prefix diff --git a/tests/utils/test_get_our_pubkeys.py b/tests/utils/test_get_our_pubkeys.py index ab11cfd..35b58d7 100644 --- a/tests/utils/test_get_our_pubkeys.py +++ b/tests/utils/test_get_our_pubkeys.py @@ -7,12 +7,23 @@ class Web3Signer: @staticmethod def load_pubkeys() -> set[str]: - return {"0xccc", "0xddd", "0xeee"} + return { + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + } def test_get_our_pubkeys() -> None: pubkey_path = Path(assets.__file__).parent / "pubkeys.txt" web3signer = Web3Signer() - expected = {"0xaaa", "0xbbb", "0xccc", "0xddd", "0xeee"} + expected = { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + } assert get_our_pubkeys(pubkey_path, web3signer) == expected # type: ignore diff --git a/tests/utils/test_is_eth1_address.py b/tests/utils/test_is_eth1_address.py deleted file mode 100644 index 0609cb3..0000000 --- a/tests/utils/test_is_eth1_address.py +++ /dev/null @@ -1,17 +0,0 @@ -from eth_validator_watcher.utils import is_eth1_address - - -def test_is_eth1_address() -> None: - # No `0x` prefix - assert not is_eth1_address("e688b84b23f322a994a53dbf8e15fa82cdb71127") - - # Too short - assert not is_eth1_address("0xe688b84b23f322a994a") - - # Too long - assert not is_eth1_address( - "0xe688b84b23f322a994ae688b84b23f322a994ae688b84b23f322a994a" - ) - - # OK - assert is_eth1_address("0xe688b84b23f322a994a53dbf8e15fa82cdb71127") diff --git a/tests/utils/test_load_pubkeys_from_file.py b/tests/utils/test_load_pubkeys_from_file.py index 0439255..86df758 100644 --- a/tests/utils/test_load_pubkeys_from_file.py +++ b/tests/utils/test_load_pubkeys_from_file.py @@ -6,5 +6,9 @@ def test_load_pubkeys_from_file(): pubkey_path = Path(assets.__file__).parent / "pubkeys.txt" - expected = {"0xaaa", "0xbbb", "0xccc"} + expected = { + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + } assert load_pubkeys_from_file(pubkey_path) == expected