From 363522fbea1c1d97256e7c8e87a29c377ab4fd9e Mon Sep 17 00:00:00 2001 From: Bernat Date: Wed, 20 Nov 2024 11:58:57 +0100 Subject: [PATCH 1/7] add addresses --- addresses/holesky.json | 3 ++- addresses/mainnet.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/addresses/holesky.json b/addresses/holesky.json index 9326fd8..3895e6b 100644 --- a/addresses/holesky.json +++ b/addresses/holesky.json @@ -4,5 +4,6 @@ "module_address": "0x4562c3e63c2e586cD1651B958C22F88135aCAd4f", "accounting_address": "0xc093e53e8F4b55A223c18A2Da6fA00e60DD5EFE1", "VEBO_address": "0xffDDF7025410412deaa05E3E1cE68FE53208afcb" - } + }, + "relay_allowlist_address": "0x2d86C5855581194a386941806E38cA119E50aEA3" } diff --git a/addresses/mainnet.json b/addresses/mainnet.json index 7e5ba7a..dcc004c 100644 --- a/addresses/mainnet.json +++ b/addresses/mainnet.json @@ -4,5 +4,6 @@ "module_address": "0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F", "accounting_address": "0x4d72BFF1BeaC69925F8Bd12526a39BAAb069e5Da", "VEBO_address": "0x0De4Ea0184c2ad0BacA7183356Aea5B8d5Bf5c6e" - } + }, + "relay_allowlist_address": "0xF95f069F9AD107938F6ba802a3da87892298610E" } From 06ee031dca07a3ca953c53883cece3c67549c1b6 Mon Sep 17 00:00:00 2001 From: Bernat Date: Wed, 20 Nov 2024 12:35:14 +0100 Subject: [PATCH 2/7] allowlist abi --- abis/relay_allowlist.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 abis/relay_allowlist.json diff --git a/abis/relay_allowlist.json b/abis/relay_allowlist.json new file mode 100644 index 0000000..840b0be --- /dev/null +++ b/abis/relay_allowlist.json @@ -0,0 +1 @@ +[{"name":"RelayAdded","inputs":[{"name":"uri_hash","type":"string","indexed":true},{"name":"relay","type":"tuple","components":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}],"indexed":false}],"anonymous":false,"type":"event"},{"name":"RelayRemoved","inputs":[{"name":"uri_hash","type":"string","indexed":true},{"name":"uri","type":"string","indexed":false}],"anonymous":false,"type":"event"},{"name":"AllowedListUpdated","inputs":[{"name":"allowed_list_version","type":"uint256","indexed":true}],"anonymous":false,"type":"event"},{"name":"OwnerChanged","inputs":[{"name":"new_owner","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"ManagerChanged","inputs":[{"name":"new_manager","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true},{"name":"amount","type":"uint256","indexed":false},{"name":"recipient","type":"address","indexed":true}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[{"name":"owner","type":"address"}],"outputs":[]},{"stateMutability":"view","type":"function","name":"get_relays_amount","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"view","type":"function","name":"get_owner","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_manager","inputs":[],"outputs":[{"name":"","type":"address"}]},{"stateMutability":"view","type":"function","name":"get_relays","inputs":[],"outputs":[{"name":"","type":"tuple[]","components":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}]}]},{"stateMutability":"view","type":"function","name":"get_relay_by_uri","inputs":[{"name":"relay_uri","type":"string"}],"outputs":[{"name":"","type":"tuple","components":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}]}]},{"stateMutability":"view","type":"function","name":"get_allowed_list_version","inputs":[],"outputs":[{"name":"","type":"uint256"}]},{"stateMutability":"nonpayable","type":"function","name":"add_relay","inputs":[{"name":"uri","type":"string"},{"name":"operator","type":"string"},{"name":"is_mandatory","type":"bool"},{"name":"description","type":"string"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"remove_relay","inputs":[{"name":"uri","type":"string"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"change_owner","inputs":[{"name":"owner","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"set_manager","inputs":[{"name":"manager","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"dismiss_manager","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"recover_erc20","inputs":[{"name":"token","type":"address"},{"name":"amount","type":"uint256"},{"name":"recipient","type":"address"}],"outputs":[]},{"stateMutability":"nonpayable","type":"fallback"}] \ No newline at end of file From 80e42ca4c595830f6b7bc66753a4bc0c3809f7dc Mon Sep 17 00:00:00 2001 From: Bernat Date: Wed, 20 Nov 2024 12:35:24 +0100 Subject: [PATCH 3/7] add relay-check action to main --- swarm/__main__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/swarm/__main__.py b/swarm/__main__.py index 91382eb..0312a86 100644 --- a/swarm/__main__.py +++ b/swarm/__main__.py @@ -2,7 +2,7 @@ import toml import sys import asyncio -from . import state_check, deploy, exit, local_sign +from . import state_check, deploy, exit, local_sign, relay_check from .util import load_chain_addresses if __name__ == '__main__': @@ -37,6 +37,9 @@ parser_exit_monitor.add_argument('--telegram', action='store_true', help='send telegram notifications') parser_exit_monitor.set_defaults(func=exit.automated_exit) + parser_relay_check = subparsers.add_parser('relay-check', help='Check validator relay coverage against allowed mev-boost relays') + parser_relay_check.set_defaults(func=relay_check.check_relays) + args = parser.parse_args() if hasattr(args, 'func'): From 3d8179a64edea70c6779670e8f770f67af875c11 Mon Sep 17 00:00:00 2001 From: Bernat Date: Wed, 20 Nov 2024 12:35:44 +0100 Subject: [PATCH 4/7] load relay allowlist contract in util func --- swarm/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/swarm/util.py b/swarm/util.py index 5a9af3c..24d8865 100644 --- a/swarm/util.py +++ b/swarm/util.py @@ -55,4 +55,7 @@ def load_chain_addresses(config: dict) -> dict: # Add withdrawal address config['deposit']['withdrawal_address'] = addresses['withdrawal_address'] + # Add relay allowlist address + config['relay_allowlist_address'] = addresses['relay_allowlist_address'] + return config From 30e1cb78e40d592907877f1a6f131a2b656ce1b0 Mon Sep 17 00:00:00 2001 From: Bernat Date: Wed, 20 Nov 2024 12:35:51 +0100 Subject: [PATCH 5/7] main relay-check logic --- swarm/relay_check.py | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 swarm/relay_check.py diff --git a/swarm/relay_check.py b/swarm/relay_check.py new file mode 100644 index 0000000..bb500a0 --- /dev/null +++ b/swarm/relay_check.py @@ -0,0 +1,72 @@ +import aiohttp +import asyncio +from .util import load_json_file +import os +from .connection.connection import NodeWSConnection +from .protocol.csm import CSM +async def get_whitelisted_relays(config): + """Fetch whitelisted relays from the allowlist contract""" + async with NodeWSConnection(config['rpc']['execution_address']) as con: + contract = con.get_contract( + address=config['relay_allowlist_address'], + abi=load_json_file(os.path.join(os.getcwd(), 'abis', 'relay_allowlist.json')) + ) + relays = await contract.functions.get_relays().call() + return relays + +async def check_validator_registration(session, relay_url, pubkey): + """Check if a validator is registered with a specific relay""" + url = f"{relay_url}/relay/v1/data/validator_registration" + try: + async with session.get(url, params={'pubkey': pubkey}) as response: + if response.status == 200: + return True + return False + except: + return False + +async def get_validator_keys_from_csm(config): + csm = CSM(config) + id = config['csm']['node_operator_id'] + return await csm.get_registered_keys(id) + +async def check_relays(config, args): + """Main function to check relay coverage for validators""" + print("Checking relay coverage for validators...") + + # Get whitelisted relays + relay_tuples = await get_whitelisted_relays(config) + # Extract just the URLs from the relay tuples + relay_urls = [relay[0] for relay in relay_tuples] + print(f"Found {len(relay_urls)} whitelisted relays") + + # Get validator public keys from CSM + validator_keys = await get_validator_keys_from_csm(config) + print(f"Found {len(validator_keys)} validators in CSM") + + # Check registration for each validator with each relay + async with aiohttp.ClientSession() as session: + results = {} + for validator in validator_keys: + results[validator] = {'total': 0, 'relays': []} + + for relay_url in relay_urls: + is_registered = await check_validator_registration(session, relay_url, validator) + if is_registered: + results[validator]['total'] += 1 + results[validator]['relays'].append(relay_url) + + # Print results + print("\nRelay coverage report:") + print("-" * 50) + for validator, data in results.items(): + coverage_pct = (data['total'] / len(relay_urls)) * 100 + print(f"\nValidator {validator[:12]}...") + print(f"Registered with {data['total']}/{len(relay_urls)} relays ({coverage_pct:.1f}%)") + if data['total'] < len(relay_urls): + print("Missing registrations for relays:") + missing_relays = set(relay_urls) - set(data['relays']) + for relay in missing_relays: + # Find the full relay info for prettier printing + relay_info = next(r for r in relay_tuples if r[0] == relay) + print(f" - {relay} ({relay_info[1]} - {relay_info[3]})") From 6ebf77c5685050514d6c43a73c448587a062b3da Mon Sep 17 00:00:00 2001 From: Bernat Date: Wed, 20 Nov 2024 14:47:24 +0100 Subject: [PATCH 6/7] graceful handling of request failures --- swarm/exception.py | 4 ++++ swarm/relay_check.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/swarm/exception.py b/swarm/exception.py index e559680..1c455ba 100644 --- a/swarm/exception.py +++ b/swarm/exception.py @@ -57,3 +57,7 @@ def __init__(self, *args: object) -> None: class SSHTunnelException(Exception): def __init__(self, *args: object) -> None: super().__init__(*args) + +class RelayRequestException(Exception): + def __init__(self, *args: object) -> None: + super().__init__(*args) diff --git a/swarm/relay_check.py b/swarm/relay_check.py index bb500a0..3eeacf5 100644 --- a/swarm/relay_check.py +++ b/swarm/relay_check.py @@ -4,6 +4,9 @@ import os from .connection.connection import NodeWSConnection from .protocol.csm import CSM +from .exception import RelayRequestException + + async def get_whitelisted_relays(config): """Fetch whitelisted relays from the allowlist contract""" async with NodeWSConnection(config['rpc']['execution_address']) as con: @@ -23,7 +26,7 @@ async def check_validator_registration(session, relay_url, pubkey): return True return False except: - return False + raise RelayRequestException(f"Failed to request validator registration from {relay_url}") async def get_validator_keys_from_csm(config): csm = CSM(config) From e64d1a78d9715bbcf58d3e90de7dd18bf37ef7fb Mon Sep 17 00:00:00 2001 From: Bernat Date: Wed, 20 Nov 2024 14:49:31 +0100 Subject: [PATCH 7/7] add relay check to readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 00a9eaf..060fb6b 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,12 @@ The state check subcommand will retrieve all keys registered in CSM, validator c `--delete` will attempt to remove dangling validator keys, i.e. validator keys that are present either in the validaor client or remote signer, but not in CSM. Keys registered in CSM will never be deleted. +### Relay check + +The relay check subcommand will check if the validator keys are registered with all whitelisted relays in the Lido Relay Allowlist. + +`python -m swarm relay-check` + ### Manual exit This subcommand will submit an exit request for a validator with a given public key.