Skip to content

Commit

Permalink
consume: add and utilize eth rpc class within consume rlp. (#556)
Browse files Browse the repository at this point in the history
Update docs/CHANGELOG.md

Co-authored-by: danceratopz <[email protected]>

chore: remove jwt auth as not used within `eth_` RPC calls.

chore: move BlockNumberType to init file.

chore: add 8545 specifically for the EthRPC class.

Update src/ethereum_test_tools/rpc/rpc.py

Co-authored-by: Mario Vega <[email protected]>

Update src/ethereum_test_tools/rpc/rpc.py

Co-authored-by: Mario Vega <[email protected]>

Update src/ethereum_test_tools/rpc/rpc.py

Co-authored-by: danceratopz <[email protected]>

feat(fw): update rpc methods.
  • Loading branch information
spencer-tb authored May 17, 2024
1 parent aa1a571 commit 7a34906
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 45 deletions.
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"]]

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"

0 comments on commit 7a34906

Please sign in to comment.