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
24 changes: 24 additions & 0 deletions docs/running_tests/execute/hive.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,27 @@ The genesis file is passed to the client with the appropriate configuration for
All tests will be executed in the same network, in the same client, and serially, but when the `-n auto` parameter is passed to the command, the tests can also be executed in parallel.

One important feature of the `execute hive` command is that, since there is no consensus client running in the network, the command drives the chain by the use of the Engine API to prompt the execution client to generate new blocks and include the transactions in them.

## Using `testing_buildBlockV1`

Clients that implement the `testing_buildBlockV1` endpoint can use it as an alternative to the standard Engine API block building flow. Instead of sending transactions to the mempool and building blocks through `engine_forkchoiceUpdatedVX` / `engine_getPayloadVX`, the plugin sends transactions directly inside the `testing_buildBlockV1` call, which builds a block containing exactly those transactions.

To enable this route, pass the `--use-testing-build-block` flag:

```bash
uv run execute hive --fork=Prague --use-testing-build-block
```

Or in dev mode:

```bash
./hive --dev --client go-ethereum
uv run execute hive --fork=Prague --use-testing-build-block
```

This is useful when:

- The client supports the endpoint and you want faster block building (the `--get-payload-wait-time` delay is skipped).
- You want deterministic transaction ordering in each block (transactions are included in the exact order provided).

See [Block Building with `testing_buildBlockV1`](./index.md#block-building-with-testing_buildblockv1) for architectural details.
21 changes: 21 additions & 0 deletions docs/running_tests/execute/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,24 @@ A warning is logged when `max_transactions_per_batch` exceeds 1000, as this may
- **Benchmark tests**: Tests that measure gas consumption often generate many transactions
- **Stress testing**: When intentionally testing RPC endpoint limits
- **Slow RPC endpoints**: Reduce batch size to avoid timeouts on slower endpoints

### Block Building with `testing_buildBlockV1`

By default, the `execute` plugin drives block production through the Engine API: transactions are sent to the client's mempool via `eth_sendRawTransaction`, and blocks are built using the `engine_forkchoiceUpdatedVX` / `engine_getPayloadVX` / `engine_newPayloadVX` sequence.

Clients that implement the [`testing_buildBlockV1`](https://github.com/ethereum/execution-apis/blob/main/src/testing/testing_buildBlockV1.yaml) endpoint offer an alternative route that collapses transaction submission and block building into a single RPC call. When enabled, the plugin:

1. Collects the raw RLP-encoded transactions for each batch.
2. Calls `testing_buildBlockV1` with the parent block hash, payload attributes, and the transaction list.
3. Finalizes the returned payload with `engine_newPayloadVX` and `engine_forkchoiceUpdatedVX`.

Because transactions are included directly in the built block (rather than pulled from the mempool), the standard Engine API `engine_getPayloadVX` call and the `--get-payload-wait-time` delay are both skipped.

**CLI Configuration:**

```bash
# Enable the testing_buildBlockV1 route
execute hive --fork=Prague --use-testing-build-block
```

This flag is available for both `execute hive` and `execute remote` (when an engine endpoint is configured). See [Execute Hive](./hive.md) and [Execute Remote](./remote.md) for mode-specific details.
17 changes: 17 additions & 0 deletions docs/running_tests/execute/remote.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ The JWT secret file must contain only the JWT secret as a hex string.

When an engine endpoint is provided, the test execution will use the Engine API to create new blocks and include transactions, giving you full control over the chain progression.

### Using `testing_buildBlockV1` with a Remote Engine

If the execution client supports the `testing_buildBlockV1` endpoint, you can enable it alongside the engine endpoint:

```bash
uv run execute remote --fork=Prague \
--rpc-endpoint=https://rpc.endpoint.io \
--rpc-seed-key 0x... --chain-id 12345 \
--engine-endpoint=https://engine.endpoint.io \
--engine-jwt-secret-file /path/to/jwt-secret.txt \
--use-testing-build-block
```

This flag requires `--engine-endpoint` to be set, because `engine_newPayload` and `engine_forkchoiceUpdated` are still needed to finalize blocks built by `testing_buildBlockV1`. Note that `testing_buildBlockV1` itself is served on the unauthenticated ETH RPC port.

See [Block Building with `testing_buildBlockV1`](./index.md#block-building-with-testing_buildblockv1) for architectural details.

The `execute remote` command will connect to the client via the RPC endpoint and will start executing every test in the `./tests` folder in the same way as the `execute hive` command, but instead of using the Engine API to generate blocks, it will send the transactions to the client via the RPC endpoint.

It is recommended to only run a subset of the tests when executing on a live network. To do so, a path to a specific test can be provided to the command:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ def pytest_addoption(parser: pytest.Parser) -> None:
"RPC. Default=750. Higher values may cause RPC instability."
),
)
execute_group.addoption(
"--use-testing-build-block",
action="store_true",
dest="use_testing_build_block",
default=False,
help=(
"Use testing_buildBlockV1 to build blocks with transactions "
"directly, instead of the standard Engine API flow. "
"Only for clients that implement this endpoint."
),
)

report_group = parser.getgroup(
"tests", "Arguments defining html report behavior"
Expand Down Expand Up @@ -338,6 +349,14 @@ def max_transactions_per_batch(request: pytest.FixtureRequest) -> int | None:
return request.config.getoption("max_tx_per_batch")


@pytest.fixture(scope="session")
def use_testing_build_block(
request: pytest.FixtureRequest,
) -> bool:
"""Return whether to use testing_buildBlockV1 for block building."""
return request.config.getoption("use_testing_build_block")


@pytest.fixture(scope="session")
def default_max_fee_per_gas(
request: pytest.FixtureRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
import time
from contextlib import AbstractContextManager
from pathlib import Path
from typing import Any, List
from typing import Any, List, Sequence

from filelock import FileLock

from execution_testing.base_types import Address, Hash, HexNumber
from execution_testing.base_types import (
Address,
Bytes,
Hash,
HexNumber,
)
from execution_testing.forks import Fork
from execution_testing.rpc import EngineRPC
from execution_testing.rpc import EngineRPC, TestingRPC
from execution_testing.rpc import EthRPC as BaseEthRPC
from execution_testing.rpc.rpc_types import (
ForkchoiceState,
GetPayloadResponse,
PayloadAttributes,
PayloadStatusEnum,
TransactionProtocol,
)


Expand All @@ -32,6 +39,7 @@ class ChainBuilderEthRPC(BaseEthRPC, namespace="eth"):
engine_rpc: EngineRPC
get_payload_wait_time: float
block_building_lock: FileLock
testing_rpc: TestingRPC | None

def __init__(
self,
Expand All @@ -44,6 +52,7 @@ def __init__(
initial_forkchoice_update_retries: int = 5,
transaction_wait_timeout: int = 60,
max_transactions_per_batch: int | None = None,
testing_rpc: TestingRPC | None = None,
):
"""Initialize the Ethereum RPC client for the hive simulator."""
super().__init__(
Expand All @@ -57,6 +66,7 @@ def __init__(
session_temp_folder / "chain_builder_fcu.lock"
)
self.get_payload_wait_time = get_payload_wait_time
self.testing_rpc = testing_rpc

# Send initial forkchoice updated only if we are the first worker
base_name = "eth_rpc_forkchoice_updated"
Expand Down Expand Up @@ -111,23 +121,16 @@ def transaction_polling_context(self) -> AbstractContextManager:
"""
return self.block_building_lock

def generate_block(self: "ChainBuilderEthRPC") -> None:
"""Generate a block using the Engine API."""
# Get the head block hash
head_block = self.get_block_by_number("latest")
assert head_block is not None

forkchoice_state = ForkchoiceState(
head_block_hash=head_block["hash"],
)
def _payload_attributes(self, head_block: dict) -> PayloadAttributes:
"""Build payload attributes from the current head block."""
parent_beacon_block_root = (
Hash(0)
if self.fork.header_beacon_root_required(
block_number=0, timestamp=0
)
else None
)
payload_attributes = PayloadAttributes(
return PayloadAttributes(
timestamp=HexNumber(head_block["timestamp"]) + 1,
prev_randao=Hash(0),
suggested_fee_recipient=Address(0),
Expand All @@ -150,6 +153,63 @@ def generate_block(self: "ChainBuilderEthRPC") -> None:
else None
),
)

def _finalize_payload(
self,
payload: GetPayloadResponse,
parent_beacon_block_root: Hash | None,
) -> None:
"""
Execute *payload* via ``engine_newPayload`` and set it as
the canonical head via ``engine_forkchoiceUpdated``.
"""
new_payload_args: List[Any] = [
payload.execution_payload,
]
if payload.blobs_bundle is not None:
new_payload_args.append(
payload.blobs_bundle.blob_versioned_hashes()
)
if parent_beacon_block_root is not None:
new_payload_args.append(parent_beacon_block_root)
if payload.execution_requests is not None:
new_payload_args.append(payload.execution_requests)
new_payload_version = self.fork.engine_new_payload_version()
assert new_payload_version is not None, (
"Fork does not support engine new_payload"
)
new_payload_response = self.engine_rpc.new_payload(
*new_payload_args, version=new_payload_version
)
assert new_payload_response.status == PayloadStatusEnum.VALID, (
"Payload was invalid"
)

fcu_version = self.fork.engine_forkchoice_updated_version()
assert fcu_version is not None, (
"Fork does not support engine forkchoice_updated"
)
new_forkchoice_state = ForkchoiceState(
head_block_hash=(payload.execution_payload.block_hash),
)
response = self.engine_rpc.forkchoice_updated(
new_forkchoice_state,
None,
version=fcu_version,
)
assert response.payload_status.status == PayloadStatusEnum.VALID, (
"Payload was invalid"
)

def generate_block(self: "ChainBuilderEthRPC") -> None:
"""Generate a block using the Engine API."""
head_block = self.get_block_by_number("latest")
assert head_block is not None

forkchoice_state = ForkchoiceState(
head_block_hash=head_block["hash"],
)
payload_attributes = self._payload_attributes(head_block)
forkchoice_updated_version = (
self.fork.engine_forkchoice_updated_version()
)
Expand All @@ -176,43 +236,51 @@ def generate_block(self: "ChainBuilderEthRPC") -> None:
response.payload_id,
version=get_payload_version,
)
new_payload_args: List[Any] = [new_payload.execution_payload]
if new_payload.blobs_bundle is not None:
new_payload_args.append(
new_payload.blobs_bundle.blob_versioned_hashes()
)
if parent_beacon_block_root is not None:
new_payload_args.append(parent_beacon_block_root)
if new_payload.execution_requests is not None:
new_payload_args.append(new_payload.execution_requests)
new_payload_version = self.fork.engine_new_payload_version()
assert new_payload_version is not None, (
"Fork does not support engine new_payload"
)
new_payload_response = self.engine_rpc.new_payload(
*new_payload_args, version=new_payload_version
)
assert new_payload_response.status == PayloadStatusEnum.VALID, (
"Payload was invalid"
)

new_forkchoice_state = ForkchoiceState(
head_block_hash=new_payload.execution_payload.block_hash,
)
response = self.engine_rpc.forkchoice_updated(
new_forkchoice_state,
None,
version=forkchoice_updated_version,
)
assert response.payload_status.status == PayloadStatusEnum.VALID, (
"Payload was invalid"
self._finalize_payload(
new_payload,
payload_attributes.parent_beacon_block_root,
)

def pending_transactions_handler(self) -> None:
"""
Called inside the transaction inclusion wait-loop.

This class triggers the block building process if it's still waiting
for transactions to be included.
This class triggers the block building process if it's still
waiting for transactions to be included.
"""
self.generate_block()

def send_transactions(
self,
transactions: Sequence[TransactionProtocol],
) -> List[Hash]:
"""
Send transactions to the execution client.

When ``testing_rpc`` is configured, build and finalize a
block containing *transactions* via
``testing_buildBlockV1`` instead of sending them to the
mempool with ``eth_sendRawTransaction``.
"""
if self.testing_rpc is None:
return super().send_transactions(transactions)
if not transactions:
return []

with self.block_building_lock:
head_block = self.get_block_by_number("latest")
assert head_block is not None

payload_attributes = self._payload_attributes(head_block)
new_payload = self.testing_rpc.build_block(
parent_block_hash=Hash(head_block["hash"]),
payload_attributes=payload_attributes,
transactions=transactions,
extra_data=Bytes(b""), # TODO: This is marked as optional
)
self._finalize_payload(
new_payload,
payload_attributes.parent_beacon_block_root,
)

return [tx.hash for tx in transactions]
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
)

from ...consume.simulators.helpers.ruleset import ruleset
from .chain_builder_eth_rpc import ChainBuilderEthRPC
from .chain_builder_eth_rpc import ChainBuilderEthRPC, TestingRPC


def pytest_addoption(parser: pytest.Parser) -> None:
Expand Down Expand Up @@ -398,10 +398,14 @@ def eth_rpc(
session_fork: Fork,
session_temp_folder: Path,
max_transactions_per_batch: int | None,
use_testing_build_block: bool,
) -> EthRPC:
"""Initialize ethereum RPC client for the execution client under test."""
get_payload_wait_time = request.config.getoption("get_payload_wait_time")
tx_wait_timeout = request.config.getoption("tx_wait_timeout")
testing_rpc = None
if use_testing_build_block:
testing_rpc = TestingRPC(f"http://{client.ip}:8545")
return ChainBuilderEthRPC(
rpc_endpoint=f"http://{client.ip}:8545",
fork=session_fork,
Expand All @@ -410,4 +414,5 @@ def eth_rpc(
get_payload_wait_time=get_payload_wait_time,
transaction_wait_timeout=tx_wait_timeout,
max_transactions_per_batch=max_transactions_per_batch,
testing_rpc=testing_rpc,
)
Loading