Skip to content
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
3 changes: 2 additions & 1 deletion docs/running_tests/execute/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ When executing tests with many transactions (e.g., benchmark tests), the `execut
- Transactions are sent in batches of up to 750 transactions by default
- Each batch is sent and confirmed before the next batch begins
- Progress logging shows batch number and transaction ranges
- The same limit caps every other batched JSON-RPC request (receipts, balances, code, account state)

**CLI Configuration:**

Expand All @@ -73,7 +74,7 @@ execute --max-tx-per-batch 1000 tests/

**Safety Threshold:**

A warning is logged when `max_transactions_per_batch` exceeds 1000, as this may cause RPC service instability or failures depending on the RPC endpoint's capacity.
A warning is logged when `max_batch_size` exceeds 1000, as this may cause RPC service instability or failures depending on the RPC endpoint's capacity.

**Use Cases:**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def pytest_html_report_title(report: Any) -> None:


# NOTE: ``transactions_per_block``, ``default_gas_price``, ``dry_run``,
# ``max_transactions_per_batch``, ``use_testing_build_block``,
# ``max_batch_size``, ``use_testing_build_block``,
# ``default_max_fee_per_gas``, ``default_max_priority_fee_per_gas``,
# ``default_max_fee_per_blob_gas``, ``max_priority_fee_per_gas``,
# ``max_fee_per_gas``, ``max_fee_per_blob_gas``, ``gas_price``, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def __init__(
get_payload_wait_time: float,
initial_forkchoice_update_retries: int = 5,
transaction_wait_timeout: int = 60,
max_transactions_per_batch: int | None = None,
max_batch_size: int | None = None,
request_timeout: TimeoutType = DEFAULT_REQUEST_TIMEOUT,
testing_rpc: TestingRPC | None = None,
expected_genesis_header: FixtureHeader | None = None,
Expand All @@ -138,7 +138,7 @@ def __init__(
super().__init__(
rpc_endpoint,
transaction_wait_timeout=transaction_wait_timeout,
max_transactions_per_batch=max_transactions_per_batch,
max_batch_size=max_batch_size,
request_timeout=request_timeout,
)
self.fork = fork
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ def eth_rpc(
engine_rpc: EngineRPC,
session_fork: Fork | TransitionFork,
session_temp_folder: Path,
max_transactions_per_batch: int | None,
max_batch_size: int | None,
use_testing_build_block: bool,
base_pre_genesis: Tuple[Alloc, FixtureHeader],
) -> EthRPC:
Expand All @@ -475,7 +475,7 @@ def eth_rpc(
session_temp_folder=session_temp_folder,
get_payload_wait_time=get_payload_wait_time,
transaction_wait_timeout=tx_wait_timeout,
max_transactions_per_batch=max_transactions_per_batch,
max_batch_size=max_batch_size,
testing_rpc=testing_rpc,
expected_genesis_header=base_pre_genesis[1],
)
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def eth_rpc(
engine_rpc: EngineRPC | None,
session_fork: Fork | TransitionFork,
session_temp_folder: Path,
max_transactions_per_batch: int | None,
max_batch_size: int | None,
use_testing_build_block: bool,
) -> EthRPC:
"""Initialize ethereum RPC client for the execution client under test."""
Expand All @@ -189,7 +189,7 @@ def eth_rpc(
return EthRPC(
rpc_endpoint,
transaction_wait_timeout=tx_wait_timeout,
max_transactions_per_batch=max_transactions_per_batch,
max_batch_size=max_batch_size,
)
get_payload_wait_time = request.config.getoption("get_payload_wait_time")
testing_rpc = None
Expand All @@ -205,6 +205,6 @@ def eth_rpc(
else session_temp_folder,
get_payload_wait_time=get_payload_wait_time,
transaction_wait_timeout=tx_wait_timeout,
max_transactions_per_batch=max_transactions_per_batch,
max_batch_size=max_batch_size,
testing_rpc=testing_rpc,
)
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def chain_builder_eth_rpc(
session_temp_folder=session_temp_folder,
get_payload_wait_time=1,
transaction_wait_timeout=20,
max_transactions_per_batch=10,
max_batch_size=10,
testing_rpc=TestingRPC(rpc_endpoint),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ def max_gas_limit_per_test(
return fork_at_genesis.transaction_gas_limit_cap()


# Other live-client fixtures (``max_transactions_per_batch``,
# Other live-client fixtures (``max_batch_size``,
# ``default_*``, fee fields, ``dry_run``, ...) come from
# ``shared.live_client_flags``. ``skip_cleanup`` from ``execute.pre_alloc``;
# we force it on in ``pytest_configure``.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def pytest_addoption(parser: pytest.Parser) -> None:
type=int,
default=None,
help=(
"Maximum number of transactions to send in a single batch to "
"Maximum number of calls to send in a single batch request to "
"the RPC. Default=750. Higher values may cause RPC instability."
),
)
Expand Down Expand Up @@ -200,8 +200,8 @@ def dry_run(request: pytest.FixtureRequest) -> bool:


@pytest.fixture(scope="session")
def max_transactions_per_batch(request: pytest.FixtureRequest) -> int | None:
"""Return max transactions per batch, or None for default."""
def max_batch_size(request: pytest.FixtureRequest) -> int | None:
"""Return max calls per batch request, or None for default."""
return request.config.getoption("max_tx_per_batch")


Expand Down
122 changes: 61 additions & 61 deletions packages/testing/src/execution_testing/rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,27 +213,43 @@ class BaseRPC:
simulators.
"""

OVERLOAD_THRESHOLD: int = 1000
DEFAULT_MAX_BATCH_SIZE: int = 750

namespace: ClassVar[str]
response_validation_context: Any | None
request_timeout: TimeoutType
max_batch_size: int

def __init__(
self,
url: str,
*,
response_validation_context: Any | None = None,
request_timeout: TimeoutType = DEFAULT_REQUEST_TIMEOUT,
max_batch_size: int | None = None,
):
"""
Initialize BaseRPC class with the given url.

`request_timeout` bounds every request made through this client;
- `request_timeout` bounds every request made through this client;
`None` disables the bound.
- `max_batch_size` caps how many calls in a single batch request.
"""
self.url = url
self.request_id_counter = count(1)
self.response_validation_context = response_validation_context
self.request_timeout = request_timeout
if max_batch_size is not None and max_batch_size < 1:
raise ValueError(
f"max_batch_size must be >= 1, got {max_batch_size}"
)
self.max_batch_size = max_batch_size or self.DEFAULT_MAX_BATCH_SIZE
Comment thread
LouisTsai-Csie marked this conversation as resolved.
if self.max_batch_size > self.OVERLOAD_THRESHOLD:
logger.warning(
f"max_batch_size ({max_batch_size}) exceeds safe threshold "
f"({self.OVERLOAD_THRESHOLD}) and may cause RPC instability."
)
self.session = requests.Session()

def close(self) -> None:
Expand Down Expand Up @@ -366,22 +382,13 @@ def post_request(

return JSONRPCResponse.model_validate(response.json())

def post_batch_request(
def _post_single_batch(
self,
*,
calls: Sequence[RPCCall],
extra_headers: Dict[str, str] | None = None,
timeout: TimeoutType = None,
extra_headers: Dict[str, str],
timeout: TimeoutType,
) -> List[JSONRPCResponse]:
"""
Send a JSON-RPC batch POST request to the client RPC server at port
defined in the url.

A `timeout` of `None` applies the client's `request_timeout`.
"""
if extra_headers is None:
extra_headers = {}

"""Send one batch POST and return responses in request order."""
json_rpc_requests = [
self._build_json_rpc_request(call) for call in calls
]
Expand Down Expand Up @@ -418,9 +425,36 @@ def post_batch_request(
)
results.append(response_map[json_rpc_request.id])

logger.info(f"Batch RPC: {len(results)} responses received")
return results

def post_batch_request(
self,
*,
calls: Sequence[RPCCall],
extra_headers: Dict[str, str] | None = None,
timeout: TimeoutType = None,
) -> List[JSONRPCResponse]:
"""
Send JSON-RPC batch POST requests to the client RPC server at port
defined in the url.

Responses are returned in the same order as `calls`.
"""
if not calls:
return []
if extra_headers is None:
extra_headers = {}

responses: List[JSONRPCResponse] = []
for start in range(0, len(calls), self.max_batch_size):
chunk = calls[start : start + self.max_batch_size]
responses.extend(
self._post_single_batch(chunk, extra_headers, timeout)
)

logger.info(f"Batch RPC: {len(responses)} responses received")
return responses


class BaseJwtRPC(BaseRPC):
"""
Expand Down Expand Up @@ -462,12 +496,8 @@ class EthRPC(BaseRPC):
within EEST based hive simulators.
"""

OVERLOAD_THRESHOLD: int = 1000
DEFAULT_MAX_TRANSACTIONS_PER_BATCH: int = 750

transaction_wait_timeout: int = 60
poll_interval: float = 1.0 # how often to poll for tx inclusion
max_transactions_per_batch: int = DEFAULT_MAX_TRANSACTIONS_PER_BATCH

gas_information_stale_seconds: int

Expand All @@ -482,7 +512,6 @@ def __init__(
transaction_wait_timeout: int = 60,
poll_interval: float | None = None,
gas_information_stale_seconds: int = 12,
max_transactions_per_batch: int | None = None,
**kwargs: Any,
) -> None:
"""Initialize JWT-authenticated RPC class with the given JWT secret."""
Expand Down Expand Up @@ -517,19 +546,6 @@ def __init__(
"blobBaseFee": 0.0,
}

# Transaction batching configuration
if max_transactions_per_batch is None:
max_transactions_per_batch = (
self.DEFAULT_MAX_TRANSACTIONS_PER_BATCH
)
self.max_transactions_per_batch = max_transactions_per_batch
if max_transactions_per_batch > self.OVERLOAD_THRESHOLD:
logger.warning(
f"max_transactions_per_batch ({max_transactions_per_batch}) "
f"exceeds the safe threshold ({self.OVERLOAD_THRESHOLD}). "
"This may cause RPC service instability or failures."
)

def config(self, timeout: int | None = None) -> EthConfigResponse | None:
"""
`eth_config`: Returns information about a fork configuration of the
Expand Down Expand Up @@ -838,41 +854,25 @@ def get_transaction_receipt(
).result_or_raise()

def get_transaction_receipts(
self,
transaction_hashes: Sequence[Hash],
*,
chunk_size: int = 500,
self, transaction_hashes: Sequence[Hash]
) -> List[dict[str, Any] | None]:
"""
`eth_getTransactionReceipt` batch: receipts for many transactions.

Returns one entry per input hash, in the same order (see
`post_batch_request`, which maps responses back by request id).

Requests are chunked because clients cap batch size -- geth's
`--rpc.batchrequestlimit` defaults to 1000 -- and because a single
response carrying thousands of receipts is several megabytes.
Returns one entry per input hash, in the same order.
"""
if not transaction_hashes:
return []
logger.info(
f"Batch requesting {len(transaction_hashes)} tx receipts "
f"in chunks of {chunk_size}"
)
receipts: List[dict[str, Any] | None] = []
for start in range(0, len(transaction_hashes), chunk_size):
chunk = transaction_hashes[start : start + chunk_size]
responses = self.post_batch_request(
calls=[
RPCCall(
method="getTransactionReceipt",
params=[f"{tx_hash}"],
)
for tx_hash in chunk
]
logger.info(f"Batch requesting {len(transaction_hashes)} tx receipts")
calls = [
RPCCall(
method="getTransactionReceipt",
params=[f"{tx_hash}"],
)
receipts.extend(r.result_or_raise() for r in responses)
return receipts
for tx_hash in transaction_hashes
]
responses = self.post_batch_request(calls=calls)
return [r.result_or_raise() for r in responses]

def get_storage_at(
self,
Expand Down Expand Up @@ -1297,7 +1297,7 @@ def send_wait_transactions(
block. Transactions are sent in batches to avoid RPC overload.
"""
results: List[Any] = []
batch_size = self.max_transactions_per_batch
batch_size = self.max_batch_size
total_txs = len(transactions)

for i in range(0, total_txs, batch_size):
Expand Down
Loading
Loading