Skip to content

Instantly share code, notes, and snippets.

@skhomuti
Created February 26, 2025 09:24
Show Gist options
  • Save skhomuti/bc188884f6bdc1ce2c65a949e84b0dc5 to your computer and use it in GitHub Desktop.
Save skhomuti/bc188884f6bdc1ce2c65a949e84b0dc5 to your computer and use it in GitHub Desktop.
Check fee recipient on the relays for CSM Node Operators
# pre-requisite: python3, web3, requests
# usage: python3 check_no_relays.py
import os
from json import JSONDecodeError
import requests
from requests.adapters import HTTPAdapter, Retry
from web3 import Web3
# Set up web3 connection
RPC_URL = os.getenv("RPC_URL")
web3 = Web3(Web3.HTTPProvider(RPC_URL))
# Contract details
CSM_ADDRESS = "0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F"
RELAYS_ALLOWED_LIST = "0xF95f069F9AD107938F6ba802a3da87892298610E"
EXPECTED_FEE_RECIPIENT = "0x388c818ca8b9251b393131c08a736a67ccb19297"
CSM_ABI = '''
[
{
"constant": true,
"inputs": [{"name": "_id", "type": "uint256"}],
"name": "getNodeOperator",
"outputs": [{
"components": [
{
"internalType": "uint32",
"name": "totalAddedKeys",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "totalWithdrawnKeys",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "totalDepositedKeys",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "totalVettedKeys",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "stuckValidatorsCount",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "depositableValidatorsCount",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "targetLimit",
"type": "uint32"
},
{
"internalType": "uint8",
"name": "targetLimitMode",
"type": "uint8"
},
{
"internalType": "uint32",
"name": "totalExitedKeys",
"type": "uint32"
},
{
"internalType": "uint32",
"name": "enqueuedCount",
"type": "uint32"
},
{
"internalType": "address",
"name": "managerAddress",
"type": "address"
},
{
"internalType": "address",
"name": "proposedManagerAddress",
"type": "address"
},
{
"internalType": "address",
"name": "rewardAddress",
"type": "address"
},
{
"internalType": "address",
"name": "proposedRewardAddress",
"type": "address"
},
{
"internalType": "bool",
"name": "extendedManagerPermissions",
"type": "bool"
}],
"internalType": "struct NodeOperator",
"name": "",
"type": "tuple"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{"name": "_id", "type": "uint256"}, {"name": "start", "type": "uint256"}, {"name": "count", "type": "uint256"}],
"name": "getSigningKeys",
"outputs": [{"name": "", "type": "bytes"}],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
'''
RELAYS_ABI = '''
[
{
"constant": true,
"inputs": [],
"name": "get_relays",
"outputs": [
{
"components": [
{"name": "uri", "type": "string"},
{"name": "operator", "type": "string"},
{"name": "is_mandatory", "type": "bool"},
{"name": "description", "type": "string"}
],
"name": "",
"type": "tuple[]"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]
'''
# Instantiate contract
csm = web3.eth.contract(address=CSM_ADDRESS, abi=CSM_ABI)
relays_allowed_list = web3.eth.contract(address=RELAYS_ALLOWED_LIST, abi=RELAYS_ABI)
def fetch_signing_keys(node_operator_id):
print("Fetching singing keys...")
# Get total deposited keys
node_operator = csm.functions.getNodeOperator(node_operator_id).call()
total_keys = node_operator[0] # Assuming it's the first return value
if total_keys == 0:
print(f"No keys found for node operator {node_operator_id}")
return []
# Fetch signing keys in batches of 50
batch_size = 50
signing_keys = []
for start in range(0, total_keys, batch_size):
batch_keys = csm.functions.getSigningKeys(node_operator_id, start, min(batch_size, total_keys - start)).call()
decoded_keys = ["0x" + batch_keys[i:i + 48].hex() for i in range(0, len(batch_keys), 48)] # Convert to hex string
signing_keys.extend(decoded_keys)
return signing_keys
def fetch_relays():
print("Fetching relays...")
return [relay[0] for relay in relays_allowed_list.functions.get_relays().call()]
def fetch_fee_recipient(session, relay, key):
url = relay + "/relay/v1/data/validator_registration?pubkey=" + key
response = session.get(url)
try:
data = response.json()
except JSONDecodeError:
print(f"Failed to decode data from {url}", response.text)
return None
message = data["message"]
if isinstance(message, str) and message.startswith("no registration found for validator"):
return None
return message.get("fee_recipient")
def check_fee_recipient(node_operator_id):
relays = fetch_relays()
keys = fetch_signing_keys(node_operator_id)
# Set up session with retries
session = requests.Session()
retries = Retry(total=5, backoff_factor=1, raise_on_status=False)
adapter = HTTPAdapter(max_retries=retries)
session.mount("http://", adapter)
session.mount("https://", adapter)
for relay in relays:
print(f"Checking relay {relay}...\n")
for key in keys:
fee_recipient = fetch_fee_recipient(session, relay, key)
if not fee_recipient:
print("No registration found for key " + key)
continue
if fee_recipient.lower() != EXPECTED_FEE_RECIPIENT:
print(f"[ERROR] Fee recipient for key {key} is incorrect\n",
f"Check it here: " + relay + "/relay/v1/data/validator_registration?pubkey=" + key)
else:
print("[OK] Fee recipient for key " + key + " is correct.")
if __name__ == "__main__":
node_operator_id = int(input("Enter node operator ID: "))
check_fee_recipient(node_operator_id)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment