Skip to content

Commit

Permalink
consume: add and utilize eth rpc class within consume rlp.
Browse files Browse the repository at this point in the history
  • Loading branch information
spencer-tb committed May 15, 2024
1 parent 0dee752 commit f57da45
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 44 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ 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 and utilize an ethereum RPC client class within consume rlp ([#556](https://github.com/ethereum/execution-spec-tests/pull/556)).


Check failure on line 28 in docs/CHANGELOG.md

View workflow job for this annotation

GitHub Actions / build (macos-latest, 3.11, 0.8.22, main, tox run-parallel --parallel-no-spinner)

Multiple consecutive blank lines

docs/CHANGELOG.md:28 MD012/no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md012.md
### 🔧 EVM Tools

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

import time
from typing import Dict, List, Literal, Union

import requests
from jwt import encode
from tenacity import retry, stop_after_attempt, wait_exponential

from ethereum_test_tools import Address


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

def __init__(self, client):
self.client = client
self.url = f"http://{client.ip}:8551"
self.jwt_secret = (
b"secretsecretsecretsecretsecretse" # oh wow, guess its not a secret anymore
)

def generate_jwt_token(self):
"""
Generates a JWT token based on the issued at timestamp and JWT secret.
"""
iat = int(time.time())
return encode({"iat": iat}, self.jwt_secret, algorithm="HS256")

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
def post_request(self, method, params):
"""
Sends a JSON-RPC POST request to the client RPC server at port 8551.
"""
payload = {
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1,
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.generate_jwt_token()}",
}

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 = error_info["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.
"""

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.
"""
if isinstance(block_number, int):
block_number
results: Dict = {}
for key in keys:
storage_value = self.get_storage_at(account, key, block_number)
results[key] = storage_value
return results
54 changes: 10 additions & 44 deletions tests_consume/test_via_rlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,24 @@
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.spec.blockchain.types import Fixture, FixtureHeader
from pytest_plugins.consume.hive_ruleset import ruleset
from pytest_plugins.consume.json_rpc import EthRPC


class TestCaseTimingData(BaseModel):
Expand Down Expand Up @@ -181,45 +181,12 @@ 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)


def compare_models(expected: FixtureHeader, got: FixtureHeader) -> dict:
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 f57da45

Please sign in to comment.