Skip to content

Commit 50654cc

Browse files
committed
parametrized rpc endpoint for majority mode
1 parent 9ed31ad commit 50654cc

File tree

3 files changed

+60
-79
lines changed

3 files changed

+60
-79
lines changed

src/ethereum_test_rpc/rpc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ def post_request(
104104
}
105105
headers = base_header | self.extra_headers | extra_headers
106106

107-
print(f"Sending RPC request to {self.url}, timeout is set to {timeout}...")
107+
# print(f"Sending RPC request to {self.url}, timeout is set to {timeout}...")
108+
print(f"Sending RPC request, timeout is set to {timeout}...") # don't leak url in logs
108109
response = requests.post(self.url, json=json, headers=headers, timeout=timeout)
109110
response.raise_for_status()
110111
response_json = response.json()

src/pytest_plugins/execute/eth_config/eth_config.py

Lines changed: 28 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import re
44
from os.path import realpath
55
from pathlib import Path
6-
from typing import Dict, List, Tuple
6+
from typing import Dict, List
77

88
import pytest
9-
import requests
109

1110
from ethereum_test_rpc import EthRPC
1211

@@ -78,6 +77,7 @@ def pytest_configure(config: pytest.Config) -> None:
7877
network_name = config.getoption("network")
7978
network_configs_path = config.getoption("network_config_file")
8079
clients = config.getoption("clients")
80+
rpc_endpoint = config.getoption("rpc_endpoint")
8181

8282
# set flags for defining whether to run majority eth_config test or not, and how
8383
config.option.majority_eth_config_test_enabled = False
@@ -109,27 +109,29 @@ def pytest_configure(config: pytest.Config) -> None:
109109
print(f"Activating majority mode\nProvided client list: {clients}")
110110

111111
# store majority mode configuration
112-
config.option.majority_eth_config_test_enabled = True
113-
config.option.majority_clients = clients # List[str]
112+
if ".ethpandaops.io" in rpc_endpoint:
113+
print("Ethpandaops RPC detected, toggling majority test on")
114+
config.option.majority_eth_config_test_enabled = True
115+
config.option.majority_clients = clients # List[str]
116+
else:
117+
print("No ethpandaops RPC detected, majority test will not be toggled on")
114118

115119
if config.getoption("collectonly", default=False):
116120
return
117121

118122
# Test out the RPC endpoint to be able to fail fast if it's not working
119-
eth_rpc = EthRPC(config.getoption("rpc_endpoint"))
123+
eth_rpc = EthRPC(rpc_endpoint)
120124
try:
121125
print("Will now perform a connection check (request chain_id)..")
122126
chain_id = eth_rpc.chain_id()
123-
print(f"Connection check ok (successfully got chain id {chain_id} from {eth_rpc.url})")
127+
print(f"Connection check ok (successfully got chain id {chain_id})")
124128
except Exception as e:
125-
pytest.exit(f"Could not connect to RPC endpoint {config.getoption('rpc_endpoint')}: {e}")
129+
pytest.exit(f"Could not connect to RPC endpoint {rpc_endpoint}: {e}")
126130
try:
127-
print(f"Will now briefly check whether {eth_rpc.url} supports eth_config..")
131+
print("Will now briefly check whether eth_config is supported by target rpc..")
128132
eth_rpc.config()
129133
except Exception as e:
130-
pytest.exit(
131-
f"RPC endpoint {config.getoption('rpc_endpoint')} does not support `eth_config`: {e}"
132-
)
134+
pytest.exit(f"RPC endpoint {rpc_endpoint} does not support `eth_config`: {e}")
133135

134136

135137
@pytest.fixture(autouse=True, scope="session")
@@ -139,63 +141,36 @@ def rpc_endpoint(request) -> str:
139141

140142

141143
@pytest.fixture(autouse=True, scope="session")
142-
def eth_rpc(rpc_endpoint: str) -> EthRPC:
143-
"""Initialize ethereum RPC client for the execution client under test."""
144-
return EthRPC(rpc_endpoint)
145-
146-
147-
def request_eth_config(*, url: str, timeout: int = 10) -> Tuple[bool, str]: # success, response
148-
"""Request data from devnet node via JSON_RPC."""
149-
payload = {
150-
"jsonrpc": "2.0",
151-
"method": "eth_config",
152-
"params": [],
153-
"id": 1,
154-
}
155-
156-
headers = {"Content-Type": "application/json"}
157-
158-
try:
159-
# Make the request
160-
response = requests.post(url, json=payload, headers=headers, timeout=timeout)
144+
def eth_rpc(rpc_endpoint: str, request) -> Dict[str, List[EthRPC]]:
145+
"""Generate list of RPC URLs which define which clients are tested."""
146+
# generate all cl+el client combinations
147+
config = request.config
148+
el_clients: List[str] = config.getoption("majority_clients") # besu, erigon, ..
161149

162-
# Return JSON response
163-
return True, response.json()
164-
165-
except Exception as e:
166-
return False, f"error: {e}"
167-
168-
169-
def get_rpc_url_combinations_el_cl(
170-
el_clients: List[str], rpc_endpoint: str
171-
) -> None | Dict[str, List[str]]:
172-
"""Get cl+el url combinations for json rpc."""
173-
# sanity checks
174-
assert ".ethpandaops.io" in rpc_endpoint
175150
assert len(el_clients) > 0
176151
if "geth" in el_clients and "fusaka-devnet-3" in rpc_endpoint:
177-
print("fusaka-devnet-3 geth does not support eth_config")
178-
return None
152+
pytest.exit("fusaka-devnet-3 geth does not support eth_config!")
179153

180154
# generate client-specific URLs from provided rpc_endpoint (it does not matter which client the original rpc_endpoint specifies) # noqa: E501
181155
# we want all combinations of consensus and execution clients (sometimes an exec client is only reachable via a subset of consensus client combinations) # noqa: E501
182156
pattern = r"(.*?@rpc\.)([^-]+)-([^-]+)(-.*)"
183-
url_dict: Dict[str, List[str]] = {
157+
url_dict: Dict[str, List[EthRPC]] = {
184158
exec_client: [
185-
re.sub(
186-
pattern,
187-
f"\\g<1>{consensus}-{exec_client}\\g<4>",
188-
rpc_endpoint,
159+
EthRPC(
160+
re.sub(
161+
pattern,
162+
f"\\g<1>{consensus}-{exec_client}\\g<4>",
163+
rpc_endpoint,
164+
)
189165
)
190166
for consensus in CONSENSUS_CLIENTS
191167
]
192168
for exec_client in el_clients
193169
}
194170
# url_dict looks like this:
195171
# {
196-
# 'besu': ["url for grandine+besu", "url for lighthouse+besu"], ...
197-
# 'erigon': ["url for grandine+erigon", "url for lighthouse+erigon"], ...
172+
# 'besu': [<EthRPC that holds url for grandine+besu>, <EthRPC that holds url for lighthouse+besu>, ..], # noqa: E501
173+
# 'erigon': ...
198174
# ...
199175
# }
200-
201176
return url_dict

src/pytest_plugins/execute/eth_config/execute_eth_config.py

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99

1010
from ethereum_test_rpc import EthConfigResponse, EthRPC
1111

12-
from .eth_config import get_rpc_url_combinations_el_cl, request_eth_config
1312
from .types import NetworkConfig
1413

1514

1615
@pytest.fixture(scope="session")
17-
def eth_config_response(eth_rpc: EthRPC) -> EthConfigResponse | None:
18-
"""Get the `eth_config` response from the client to be verified by all tests."""
19-
return eth_rpc.config(timeout=10)
16+
def eth_config_response(request) -> EthConfigResponse | None:
17+
"""Get the `eth_config` response from the user-provided RPC endpoint."""
18+
config = request.config
19+
eth_rpc_url = config.getoption("rpc_endpoint")
20+
eth_rpc_target = EthRPC(eth_rpc_url)
21+
return eth_rpc_target.config()
2022

2123

2224
@pytest.fixture(scope="session")
@@ -209,6 +211,7 @@ def test_eth_config_last_fork_id(
209211

210212

211213
def test_eth_config_majority(
214+
eth_rpc: Dict[str, List[EthRPC]],
212215
request,
213216
) -> None:
214217
"""Queries devnet exec clients for their eth_config and fails if not all have the same response.""" # noqa: E501
@@ -218,39 +221,38 @@ def test_eth_config_majority(
218221
if not run_this_test_bool:
219222
pytest.skip("Skipping eth_config majority test")
220223

221-
# retrieve required values for running this test
222-
rpc_endpoint = config.getoption("rpc_endpoint")
223-
el_clients: List[str] = config.getoption("majority_clients") # besu, erigon, ..
224+
assert eth_rpc is not None
224225

225-
url_dict: None | Dict[str, List[str]] = get_rpc_url_combinations_el_cl(
226-
el_clients=el_clients, rpc_endpoint=rpc_endpoint
227-
)
228-
assert url_dict is not None
229-
230-
responses = dict() # noqa: C408
231-
for exec_client in url_dict.keys():
226+
responses = dict() # Dict[exec_client_name : response] # noqa: C408
227+
client_to_url_used_dict = dict() # noqa: C408
228+
for exec_client in eth_rpc.keys():
232229
# try only as many consensus+exec client combinations until you receive a response
233-
# if all combinations fail we panic
234-
for url in url_dict[exec_client]:
235-
success, response = request_eth_config(url=url, timeout=9)
236-
if not success:
230+
# if all combinations for a given exec client fail we panic
231+
for eth_rpc_target in eth_rpc[exec_client]:
232+
response = eth_rpc_target.config(timeout=10)
233+
if response is None:
237234
# safely split url to not leak rpc_endpoint in logs
238235
print(
239-
f"When trying to get eth_config from {url.split('@', 1)[-1] if '@' in url else ''} the following problem occurred: {response}" # noqa: E501
236+
f"When trying to get eth_config from {eth_rpc_target} a problem occurred" # problem itself is logged by .config() call # noqa: E501
240237
)
241238
continue
242239

243-
responses[exec_client] = response
244-
print(f"Response of {exec_client}: {response}\n\n")
240+
response_str = json.dumps(response.model_dump(mode="json"))
241+
responses[exec_client] = response_str
242+
client_to_url_used_dict[exec_client] = (
243+
eth_rpc_target.url
244+
) # remember which cl+el combination was used # noqa: E501
245+
print(f"Response of {exec_client}: {response_str}\n\n")
246+
245247
break # no need to gather more responses for this client
246248

247-
assert len(responses.keys()) == len(el_clients), "Failed to get an eth_config response "
248-
f" from each specified execution client. Full list of execution clients is {el_clients} "
249+
assert len(responses.keys()) == len(eth_rpc.keys()), "Failed to get an eth_config response "
250+
f" from each specified execution client. Full list of execution clients is {eth_rpc.keys()} "
249251
f"but we were only able to gather eth_config responses from: {responses.keys()}\nWill try "
250252
"again with a different consensus-execution client combination for this execution client"
251253

252254
# determine hashes of client responses
253-
client_to_hash_dict = dict() # noqa: C408
255+
client_to_hash_dict = dict() # Dict[exec_client : response hash] # noqa: C408
254256
for client in responses.keys():
255257
response_bytes = json.dumps(responses[client], sort_keys=True).encode("utf-8")
256258
response_hash = sha256(response_bytes).digest().hex()
@@ -265,7 +267,10 @@ def test_eth_config_majority(
265267
continue
266268

267269
assert client_to_hash_dict[h] == expected_hash, (
268-
"Critical consensus issue: Not all eth_config responses are the same!"
270+
"Critical consensus issue: Not all eth_config responses are the same! "
271+
f"Here is an overview of client response hashes:\n{'\n\t'.join(f'{k}: {v}' for k, v in client_to_hash_dict.items())}\n\n" # noqa: E501
272+
f"Here is an overview of which URLs were contacted:\n\t{'\n\t'.join(f'{k}: @{v.split("@")[1]}' for k, v in client_to_url_used_dict.items())}\n\n" # log which cl+el combinations were used without leaking full url # noqa: E501
273+
f"Here is a dump of all client responses:\n{'\n\n'.join(f'{k}: {v}' for k, v in responses.items())}" # noqa: E501
269274
)
270275
assert expected_hash != ""
271276

0 commit comments

Comments
 (0)