Skip to content

Commit 9aefc86

Browse files
feat(test-execute): tx batching for execute remote (#1907)
* feat: implement rpc rate limit * docs: add transaction batching for execute remote * docs: enhance transaction execution with batching support * refactor: move rate limit to rpc class
1 parent 05def83 commit 9aefc86

9 files changed

Lines changed: 115 additions & 4 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Test fixtures for use by clients are available for each release on the [Github r
1818

1919
#### `execute`
2020

21+
- ✨ Add transaction batching to avoid RPC overload when executing tests with many transactions. Transactions are now sent in configurable batches (default: 750) with progress logging. Use `--max-tx-per-batch` to configure the batch size ([#1907](https://github.com/ethereum/execution-specs/pull/1907)).
2122
-`execute hive` and `execute remote` now defer funding of accounts until the minimum amount required to send the test transactions is calculated, in order to optimize the amount of Eth used to execute the tests ([#1822](https://github.com/ethereum/execution-specs/pull/1822)).
2223
- ✨ Dynamically fetch gas prices from the network and update all transactions to use 1.5x the current values ([#1822](https://github.com/ethereum/execution-specs/pull/1822)).
2324
- ✨ New `--dry-run` flag to calculate the amount of Eth that will be spent executing a test given the current network gas prices ([#1822](https://github.com/ethereum/execution-specs/pull/1822)).

docs/running_tests/execute/index.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,35 @@ EOAs are funded after gas prices are determined, enabling accurate balance calcu
4848
### Blob Transaction Support
4949

5050
Blob transactions are fully supported in execute mode, including automatic gas pricing for blob gas fees and validation via `engine_getBlobsVX` endpoints when the Engine RPC is available.
51+
52+
### Transaction Batching
53+
54+
When executing tests with many transactions (e.g., benchmark tests), the `execute` plugin automatically batches transactions to avoid overloading the RPC service (The experiment transaction limit for RPC is 1000 requests.). This is particularly important for large-scale tests that may generate hundreds or thousands of transactions.
55+
56+
**Default Behavior:**
57+
58+
- Transactions are sent in batches of up to 750 transactions by default
59+
- Each batch is sent and confirmed before the next batch begins
60+
- Progress logging shows batch number and transaction ranges
61+
62+
**CLI Configuration:**
63+
64+
The batch size can be configured via the `--max-tx-per-batch` option:
65+
66+
```bash
67+
# Use smaller batches for slower RPC endpoints
68+
execute --max-tx-per-batch 100 tests/
69+
70+
# Use larger batches for high-performance RPC endpoints
71+
execute --max-tx-per-batch 1000 tests/
72+
```
73+
74+
**Safety Threshold:**
75+
76+
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.
77+
78+
**Use Cases:**
79+
80+
- **Benchmark tests**: Tests that measure gas consumption often generate many transactions
81+
- **Stress testing**: When intentionally testing RPC endpoint limits
82+
- **Slow RPC endpoints**: Reduce batch size to avoid timeouts on slower endpoints

docs/running_tests/execute/remote.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,22 @@ Once the sender account is funded, the command will start executing tests one by
212212

213213
Test transactions are not sent from the main sender account though, they are sent from a different unique account that is created for each test (accounts returned by `pre.fund_eoa`).
214214

215+
### Transaction Batching
216+
217+
When executing tests that generate many transactions (such as benchmark tests), transactions are automatically batched to avoid overloading the RPC endpoint. By default, transactions are sent in batches of 750.
218+
219+
You can configure the batch size using the `--max-tx-per-batch` flag:
220+
221+
```bash
222+
# Reduce batch size for slower RPC endpoints
223+
uv run execute remote --fork=Prague --rpc-endpoint=https://rpc.endpoint.io --max-tx-per-batch 100 --rpc-seed-key 0x... --chain-id 12345
224+
225+
# Increase batch size for high-performance endpoints
226+
uv run execute remote --fork=Prague --rpc-endpoint=https://rpc.endpoint.io --max-tx-per-batch 1000 --rpc-seed-key 0x... --chain-id 12345
227+
```
228+
229+
A warning is logged when the batch size exceeds 1000, as this may cause RPC service instability.
230+
215231
### Use with Parallel Execution
216232

217233
If the `execute` is run using the `-n=N` flag (respectively `--sim-parallelism=N`), n>1, the tests will be executed in parallel, and each process will have its own separate sender account, so the amount that is swept from the seed account is divided by the number of processes, and this has to be taken into account when setting the sweep amount and also when funding the seed account.

packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ def pytest_addoption(parser: pytest.Parser) -> None:
138138
default=False,
139139
help="Don't send transactions, just print the minimum balance required per test.",
140140
)
141+
execute_group.addoption(
142+
"--max-tx-per-batch",
143+
action="store",
144+
dest="max_tx_per_batch",
145+
type=int,
146+
default=None,
147+
help=(
148+
"Maximum number of transactions to send in a single batch to the RPC. "
149+
"Default=750. Higher values may cause RPC instability."
150+
),
151+
)
141152

142153
report_group = parser.getgroup(
143154
"tests", "Arguments defining html report behavior"
@@ -311,6 +322,12 @@ def dry_run(request: pytest.FixtureRequest) -> bool:
311322
return request.config.getoption("dry_run")
312323

313324

325+
@pytest.fixture(scope="session")
326+
def max_transactions_per_batch(request: pytest.FixtureRequest) -> int | None:
327+
"""Return the maximum number of transactions per batch, or None for default."""
328+
return request.config.getoption("max_tx_per_batch")
329+
330+
314331
@pytest.fixture(scope="session")
315332
def default_max_fee_per_gas(
316333
request: pytest.FixtureRequest,

packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/chain_builder_eth_rpc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +205,13 @@ def __init__(
205205
get_payload_wait_time: float,
206206
initial_forkchoice_update_retries: int = 5,
207207
transaction_wait_timeout: int = 60,
208+
max_transactions_per_batch: int | None = None,
208209
):
209210
"""Initialize the Ethereum RPC client for the hive simulator."""
210211
super().__init__(
211212
rpc_endpoint,
212213
transaction_wait_timeout=transaction_wait_timeout,
214+
max_transactions_per_batch=max_transactions_per_batch,
213215
)
214216
self.fork = fork
215217
self.engine_rpc = engine_rpc

packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/hive.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ def eth_rpc(
383383
session_fork: Fork,
384384
transactions_per_block: int,
385385
session_temp_folder: Path,
386+
max_transactions_per_batch: int | None,
386387
) -> EthRPC:
387388
"""Initialize ethereum RPC client for the execution client under test."""
388389
get_payload_wait_time = request.config.getoption("get_payload_wait_time")
@@ -395,4 +396,5 @@ def eth_rpc(
395396
session_temp_folder=session_temp_folder,
396397
get_payload_wait_time=get_payload_wait_time,
397398
transaction_wait_timeout=tx_wait_timeout,
399+
max_transactions_per_batch=max_transactions_per_batch,
398400
)

packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/rpc/remote.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,16 @@ def eth_rpc(
161161
session_fork: Fork,
162162
transactions_per_block: int,
163163
session_temp_folder: Path,
164+
max_transactions_per_batch: int | None,
164165
) -> EthRPC:
165166
"""Initialize ethereum RPC client for the execution client under test."""
166167
tx_wait_timeout = request.config.getoption("tx_wait_timeout")
167168
if engine_rpc is None:
168-
return EthRPC(rpc_endpoint, transaction_wait_timeout=tx_wait_timeout)
169+
return EthRPC(
170+
rpc_endpoint,
171+
transaction_wait_timeout=tx_wait_timeout,
172+
max_transactions_per_batch=max_transactions_per_batch,
173+
)
169174
get_payload_wait_time = request.config.getoption("get_payload_wait_time")
170175
return ChainBuilderEthRPC(
171176
rpc_endpoint=rpc_endpoint,
@@ -175,4 +180,5 @@ def eth_rpc(
175180
session_temp_folder=session_temp_folder,
176181
get_payload_wait_time=get_payload_wait_time,
177182
transaction_wait_timeout=tx_wait_timeout,
183+
max_transactions_per_batch=max_transactions_per_batch,
178184
)

packages/testing/src/execution_testing/execution/transaction_post.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def execute(
8080
"""Execute the format."""
8181
del fork
8282
del engine_rpc
83+
8384
for block in self.blocks:
8485
for tx in block:
8586
if not isinstance(tx, NetworkWrappedTransaction):
@@ -131,6 +132,7 @@ def execute(
131132
f"Transaction rejected as expected: {exc_info.value}"
132133
)
133134
else:
135+
# Send transactions (batching is handled by eth_rpc internally)
134136
eth_rpc.send_wait_transactions(signed_txs)
135137
all_tx_hashes.extend([tx.hash for tx in signed_txs])
136138

packages/testing/src/execution_testing/rpc/rpc.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,12 @@ class EthRPC(BaseRPC):
267267
within EEST based hive simulators.
268268
"""
269269

270+
OVERLOAD_THRESHOLD: int = 1000
271+
DEFAULT_MAX_TRANSACTIONS_PER_BATCH: int = 750
272+
270273
transaction_wait_timeout: int = 60
271274
poll_interval: float = 1.0 # how often to poll for tx inclusion
275+
max_transactions_per_batch: int = DEFAULT_MAX_TRANSACTIONS_PER_BATCH
272276

273277
gas_information_stale_seconds: int
274278

@@ -283,6 +287,7 @@ def __init__(
283287
transaction_wait_timeout: int = 60,
284288
poll_interval: float | None = None,
285289
gas_information_stale_seconds: int = 12,
290+
max_transactions_per_batch: int | None = None,
286291
**kwargs: Any,
287292
) -> None:
288293
"""
@@ -320,6 +325,19 @@ def __init__(
320325
"blobBaseFee": 0.0,
321326
}
322327

328+
# Transaction batching configuration
329+
if max_transactions_per_batch is None:
330+
max_transactions_per_batch = (
331+
self.DEFAULT_MAX_TRANSACTIONS_PER_BATCH
332+
)
333+
self.max_transactions_per_batch = max_transactions_per_batch
334+
if max_transactions_per_batch > self.OVERLOAD_THRESHOLD:
335+
logger.warning(
336+
f"max_transactions_per_batch ({max_transactions_per_batch}) exceeds "
337+
f"the safe threshold ({self.OVERLOAD_THRESHOLD}). "
338+
"This may cause RPC service instability or failures."
339+
)
340+
323341
def config(self, timeout: int | None = None) -> EthConfigResponse | None:
324342
"""
325343
`eth_config`: Returns information about a fork configuration of the
@@ -707,10 +725,25 @@ def send_wait_transactions(
707725
) -> List[Any]:
708726
"""
709727
Send list of transactions and waits until all of them are included in a
710-
block.
728+
block. Transactions are sent in batches to avoid RPC overload.
711729
"""
712-
self.send_transactions(transactions)
713-
return self.wait_for_transactions(transactions)
730+
results: List[Any] = []
731+
batch_size = self.max_transactions_per_batch
732+
total_txs = len(transactions)
733+
734+
for i in range(0, total_txs, batch_size):
735+
batch = transactions[i : i + batch_size]
736+
if total_txs > batch_size:
737+
logger.info(
738+
f"Sending transaction batch {i // batch_size + 1} "
739+
f"({len(batch)} transactions, "
740+
f"{i + 1}-{min(i + batch_size, total_txs)} "
741+
f"of {total_txs})"
742+
)
743+
self.send_transactions(batch)
744+
results.extend(self.wait_for_transactions(batch))
745+
746+
return results
714747

715748

716749
class DebugRPC(EthRPC):

0 commit comments

Comments
 (0)