Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(fw): Utilize an EthRPC class within consume rlp #556

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ Added `--traces` support when running with Hyperledger Besu ([#511](https://github.com/ethereum/execution-spec-tests/pull/511)).
- ✨ Use pytest's "short" traceback style (`--tb=short`) for failure summaries in the test report for more compact terminal output ([#542](https://github.com/ethereum/execution-spec-tests/pull/542)).
- ✨ The `fill` command now generates HTML test reports with links to the JSON fixtures and debug information ([#537](https://github.com/ethereum/execution-spec-tests/pull/537)).
- ✨ Add an Ethereum RPC client class for use with consume commands ([#556](https://github.com/ethereum/execution-spec-tests/pull/556)).

### 🔧 EVM Tools

Expand Down
7 changes: 7 additions & 0 deletions src/ethereum_test_tools/rpc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
JSON-RPC methods and helper functions for EEST consume based hive simulators.
"""

from .rpc import BlockNumberType, EthRPC

__all__ = ["EthRPC", "BlockNumberType"]
115 changes: 115 additions & 0 deletions src/ethereum_test_tools/rpc/rpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
JSON-RPC methods and helper functions for EEST consume based hive simulators.
"""

from abc import ABC
from typing import Any, Dict, List, Literal, Optional, Union

import requests
from tenacity import retry, stop_after_attempt, wait_exponential

from ethereum_test_tools import Address

BlockNumberType = Union[int, Literal["latest", "earliest", "pending"]]


class BaseRPC(ABC):
"""
Represents a base RPC class for every RPC call used within EEST based hive simulators.
"""

def __init__(self, client_ip: str, port: int):
self.ip = client_ip
self.url = f"http://{client_ip}:{port}"

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
def post_request(
self, method: str, params: List[Any], extra_headers: Optional[Dict] = None
) -> Dict:
"""
Sends a JSON-RPC POST request to the client RPC server at port defined in the url.
"""
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
}
base_header = {
"Content-Type": "application/json",
}
headers = base_header if extra_headers is None else {**base_header, **extra_headers}

response = requests.post(self.url, json=payload, headers=headers)
response.raise_for_status()
result = response.json().get("result")

if result is None or "error" in result:
error_info = "result is None; and therefore contains no error info"
error_code = None
if result is not None:
error_info = result["error"]
error_code = result["error"]["code"]
raise Exception(
f"Error calling JSON RPC {method}, code: {error_code}, " f"message: {error_info}"
)

return result


class EthRPC(BaseRPC):
"""
Represents an `eth_X` RPC class for every default ethereum RPC method used within EEST based
hive simulators.
"""

def __init__(self, client_ip):
"""
Initializes the EthRPC class with the http port 8545, which requires no authentication.
"""
super().__init__(client_ip, port=8545)

BlockNumberType = Union[int, Literal["latest", "earliest", "pending"]]
danceratopz marked this conversation as resolved.
Show resolved Hide resolved

def get_block_by_number(self, block_number: BlockNumberType = "latest", full_txs: bool = True):
"""
`eth_getBlockByNumber`: Returns information about a block by block number.
"""
block = hex(block_number) if isinstance(block_number, int) else block_number
return self.post_request("eth_getBlockByNumber", [block, full_txs])

def get_balance(self, address: str, block_number: BlockNumberType = "latest"):
"""
`eth_getBalance`: Returns the balance of the account of given address.
"""
block = hex(block_number) if isinstance(block_number, int) else block_number
return self.post_request("eth_getBalance", [address, block])

def get_transaction_count(self, address: Address, block_number: BlockNumberType = "latest"):
"""
`eth_getTransactionCount`: Returns the number of transactions sent from an address.
"""
block = hex(block_number) if isinstance(block_number, int) else block_number
return self.post_request("eth_getTransactionCount", [address, block])

def get_storage_at(
self, address: str, position: str, block_number: BlockNumberType = "latest"
):
"""
`eth_getStorageAt`: Returns the value from a storage position at a given address.
"""
block = hex(block_number) if isinstance(block_number, int) else block_number
return self.post_request("eth_getStorageAt", [address, position, block])

def storage_at_keys(
self, account: str, keys: List[str], block_number: BlockNumberType = "latest"
) -> Dict:
"""
Helper to retrieve the storage values for the specified keys at a given address and block
number.
"""
results: Dict = {}
for key in keys:
storage_value = self.get_storage_at(account, key, block_number)
results[key] = storage_value
return results
56 changes: 11 additions & 45 deletions tests_consume/test_via_rlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@
1. The client's genesis block hash matches that defined in the fixture.
2. The client's last block hash matches that defined in the fixture.
"""

import io
import json
import pprint
import time
from typing import Any, Generator, List, Literal, Mapping, Optional, Union, cast
from typing import Generator, List, Mapping, Optional, cast

import pytest
import requests
import rich
from hive.client import Client, ClientType
from hive.testing import HiveTest
from pydantic import BaseModel
from tenacity import retry, stop_after_attempt, wait_exponential

from ethereum_test_tools.common.base_types import Bytes
from ethereum_test_tools.common.json import to_json
from ethereum_test_tools.rpc import EthRPC
from ethereum_test_tools.spec.blockchain.types import Fixture, FixtureHeader
from pytest_plugins.consume.hive_ruleset import ruleset

Expand Down Expand Up @@ -181,53 +181,20 @@ def client(
timing_data.stop_client = time.perf_counter() - t_start


BlockNumberType = Union[int, Literal["latest", "earliest", "pending"]]


@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
def get_block(client: Client, block_number: BlockNumberType) -> dict:
@pytest.fixture(scope="function")
def eth_rpc(client: Client) -> EthRPC:
"""
Retrieve the i-th block from the client using the JSON-RPC API.
Retries up to two times (three attempts total) in case of an error or a timeout,
with exponential backoff.
Initialize ethereum RPC client for the execution client under test.
"""
if isinstance(block_number, int):
block_number_string = hex(block_number)
else:
block_number_string = block_number
url = f"http://{client.ip}:8545"
payload = {
"jsonrpc": "2.0",
"method": "eth_getBlockByNumber",
"params": [block_number_string, False],
"id": 1,
}
headers = {"Content-Type": "application/json"}

response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
result = response.json().get("result")

if result is None or "error" in result:
error_info: Any = "result is None; and therefore contains no error info"
error_code = None
if result is not None:
error_info = result["error"]
error_code = error_info["code"]
raise Exception(
f"Error calling JSON RPC eth_getBlockByNumber, code: {error_code}, "
f"message: {error_info}"
)

return result
return EthRPC(client_ip=client.ip)


def compare_models(expected: FixtureHeader, got: FixtureHeader) -> dict:
"""
Compare two FixtureHeader model instances and return their differences.
"""
differences = {}
for (exp_name, exp_value), (got_name, got_value) in zip(expected, got):
for (exp_name, exp_value), (_, got_value) in zip(expected, got):
if exp_value != got_value:
differences[exp_name] = {
"expected ": str(exp_value),
Expand Down Expand Up @@ -262,9 +229,8 @@ def __init__(self, *, expected_header: FixtureHeader, got_header: FixtureHeader)


def test_via_rlp(
client: Client,
eth_rpc: EthRPC,
fixture: Fixture,
client_genesis: dict,
timing_data: TestCaseTimingData,
):
"""
Expand All @@ -277,12 +243,12 @@ def test_via_rlp(
2. The client's last block's hash matches `fixture.last_block_hash`.
"""
t_start = time.perf_counter()
genesis_block = get_block(client, 0)
genesis_block = eth_rpc.get_block_by_number(0)
timing_data.get_genesis = time.perf_counter() - t_start
if genesis_block["hash"] != str(fixture.genesis.block_hash):
raise GenesisBlockMismatchException(
expected_header=fixture.genesis, got_header=FixtureHeader(**genesis_block)
)
block = get_block(client, "latest")
block = eth_rpc.get_block_by_number("latest")
timing_data.get_last_block = time.perf_counter() - timing_data.get_genesis - t_start
assert block["hash"] == str(fixture.last_block_hash), "hash mismatch in last block"