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
21 changes: 17 additions & 4 deletions .github/workflows/hive-consume.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
- ".gitignore"
- ".vscode/**"
- "whitelist.txt"
- "docs/**"
- "docs/**"
- "mkdocs.yml"
pull_request:
paths:
Expand Down Expand Up @@ -71,15 +71,29 @@ jobs:
- name: Engine
mode: simulator
simulator: ethereum/eels/consume-engine
sim_limit: ".*test_block_at_rlp_limit_with_logs.*Osaka.*"
# TODO: Enable once eels/consume-enginex simulator is added to Hive
# - name: consume-enginex
# mode: simulator
# simulator: ethereum/eels/consume-enginex
# sim_limit: ".*push0.*(Shanghai|Cancun).*"
- name: RLP
mode: simulator
simulator: ethereum/eels/consume-rlp
sim_limit: ".*test_block_at_rlp_limit_with_logs.*Osaka.*"
- name: Sync
mode: simulator
simulator: ethereum/eels/consume-sync
sim_limit: ".*test_block_at_rlp_limit_with_logs.*Osaka.*"
- name: Dev Mode
mode: dev
consume_command: engine
test_filter: "Osaka and test_block_at_rlp_limit_with_logs"
# TODO: Enable once we have a release with the pre-alloc builder format cf #2140
# - name: dev-mode-enginex
# mode: dev
# consume_command: enginex
# test_filter: "(fork_shanghai or fork_cancun) and push0"
steps:
- name: Checkout execution-specs
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
Expand Down Expand Up @@ -122,7 +136,7 @@ jobs:
--client go-ethereum \
--client-file ../execution-specs/.github/configs/hive/latest.yaml \
--sim.buildarg fixtures=${{ env.FIXTURES_URL }} \
--sim.limit=".*test_block_at_rlp_limit_with_logs.*Osaka.*" \
--sim.limit="${{ matrix.sim_limit }}" \
--docker.output

- name: Start Hive in dev mode
Expand All @@ -142,5 +156,4 @@ jobs:
HIVE_SIMULATOR: ${{ steps.start-hive.outputs.hive-url }}
run: |
uv sync --all-extras
uv run consume ${{ matrix.consume_command }} --input ${{ env.FIXTURES_URL }} -k "Osaka and test_block_at_rlp_limit_with_logs"

uv run consume ${{ matrix.consume_command }} --input ${{ env.FIXTURES_URL }} -k "${{ matrix.test_filter }}"
43 changes: 43 additions & 0 deletions docs/running_tests/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Both `consume` and `execute` provide sub-commands which correspond to different
| [`consume direct`](#direct) | Client consume tests via a `statetest` interface | EVM | None | Module test |
| [`consume direct`](#direct) | Client consume tests via a `blocktest` interface | EVM, block processing | None | Module test,</br>Integration test |
| [`consume engine`](#engine) | Client imports blocks via Engine API `EngineNewPayload` in Hive | EVM, block processing, Engine API | Staging, Hive | System test |
| [`consume enginex`](#enginex) | Client imports blocks via Engine API in Hive, optimized by client reuse | EVM, block processing, Engine API | Staging, Hive | System test |
| [`consume sync`](#sync) | Client syncs from another client using Engine API in Hive | EVM, block processing, Engine API, P2P sync | Staging, Hive | System test |
| [`consume rlp`](#rlp) | Client imports RLP-encoded blocks upon start-up in Hive | EVM, block processing, RLP import (sync\*) | Staging, Hive | System test |
| [`execute hive`](./execute/hive.md) | Tests executed against a client via JSON RPC `eth_sendRawTransaction` in Hive | EVM, JSON RPC, mempool | Staging, Hive | System test |
Expand Down Expand Up @@ -62,6 +63,48 @@ The `consume engine` command:
5. **Validates responses** against expected results.
6. **Tests error conditions** and exception handling.

## EngineX

| Nomenclature | |
| -------------- | -------------------------- |
| Command | `consume enginex` |
| Simulator | `eels/consume-enginex` |
| Fixture format | `blockchain_test_engine_x` |

The EngineX method is a faster alternative to `consume engine` that executes multiple tests against a single client instance. This is achieved via the [Blockchain Engine X Test fixture format](./test_formats/blockchain_test_engine_x.md) which groups tests that share the same fork and EVM [Environment](./test_formats/state_test.md#fixtureenvironment) together and contains a larger, shared pre-allocation state that all tests in the group use. This allows the EngineX simulator to execute multiple tests against the same client instance, whereas the Engine Simulator starts a fresh client for each test.

The `consume enginex` command, for each pre-allocation group:

1. **Initializes the execution client** with the group's shared genesis state.
2. **Connects via Engine API** (port 8551).
3. **Executes all tests in the group** against the same client:

- Submits payloads from each test using `engine_newPayload` calls.
- Validates responses against expected results.
- Tests error conditions and exception handling.

4. **Stops the client** when all tests in the group complete.

### Engine vs EngineX

| | `consume engine` | `consume enginex` |
| -------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| **Fixture format** | [`blockchain_test_engine`](./test_formats/blockchain_test_engine.md) | [`blockchain_test_engine_x`](./test_formats/blockchain_test_engine_x.md) |
| **Client lifecycle** | New client per test | Client reused across tests with same pre-alloc |
| **Fork choice update** | FCU called for genesis and final payload | FCU for genesis and final payload skipped |
| **Execution speed** | Slower (client startup overhead) | Faster (amortized startup cost) |
| **Test isolation** | Full isolation | Shared genesis state within group |

EngineX achieves faster execution by:

1. **Grouping tests** by their pre-allocation state (genesis configuration).
2. **Reusing clients** across all tests in a group, avoiding repeated client startup.
3. **Skipping redundant initialization** since the client is already at the expected genesis state.

!!! note "When to use EngineX vs Engine"

Use `consume enginex` for faster test runs when full per-test isolation is not required. Use `consume engine` when you need complete isolation between tests or when debugging issues triggered by a single test case.

## RLP

| Nomenclature | |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,13 @@ def create_consume_command(
def get_command_logic_test_paths(command_name: str) -> List[Path]:
"""Determine the command paths based on the command name and hive flag."""
base_path = Path("cli/pytest_commands/plugins/consume")
if command_name in ["engine", "rlp"]:
if command_name in ["engine", "enginex", "rlp"]:
test_command = "engine" if command_name == "enginex" else command_name
command_logic_test_paths = [
base_path
/ "simulators"
/ "simulator_logic"
/ f"test_via_{command_name}.py"
/ f"test_via_{test_command}.py"
]
elif command_name == "sync":
command_logic_test_paths = [
Expand Down Expand Up @@ -119,6 +120,12 @@ def engine() -> None:
pass


@consume_command(is_hive=True)
def enginex() -> None:
"""Consume via Engine API with pre-alloc optimization."""
pass


@consume_command(is_hive=True)
def sync() -> None:
"""Client consumes via the Engine API with sync testing."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from dataclasses import dataclass
from io import BytesIO
from pathlib import Path
from typing import Any, List, Optional, Tuple
from typing import Any, List, Optional, Tuple, cast
from urllib.parse import urlparse

import platformdirs
Expand All @@ -19,8 +19,19 @@
from execution_testing.cli.gen_index import (
generate_fixtures_index,
)
from execution_testing.fixtures import BaseFixture, FixtureFormat
from execution_testing.fixtures.consume import IndexFile, TestCases
from execution_testing.cli.pytest_commands.plugins.consume.simulators.helpers.test_tracker import ( # noqa: E501
make_group_identifier,
)
from execution_testing.fixtures import (
BaseFixture,
BlockchainEngineXFixture,
FixtureFormat,
)
from execution_testing.fixtures.consume import (
IndexFile,
TestCaseBase,
TestCases,
)
from execution_testing.forks import (
get_forks,
get_relative_fork_markers,
Expand Down Expand Up @@ -596,22 +607,43 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
fork_markers = get_relative_fork_markers(
test_case.fork, strict_mode=False
)

# Build base marks (fork and format)
marks = [getattr(pytest.mark, m) for m in fork_markers] + [
getattr(pytest.mark, test_case.format.format_name)
]

param = pytest.param(
test_case,
id=test_case.id,
marks=[getattr(pytest.mark, m) for m in fork_markers]
+ [getattr(pytest.mark, test_case.format.format_name)],
marks=marks,
)
param_list.append(param)

metafunc.parametrize("test_case", param_list)
if "client_type" not in metafunc.fixturenames:
metafunc.parametrize("test_case", param_list)
return

if "client_type" in metafunc.fixturenames:
metafunc.parametrize(
"client_type",
metafunc.config.hive_execution_clients, # type: ignore[attr-defined]
ids=[
client.name
for client in metafunc.config.hive_execution_clients # type: ignore[attr-defined]
],
)
clients = metafunc.config.hive_execution_clients # type: ignore[attr-defined]
is_enginex = BlockchainEngineXFixture in supported_fixture_formats
combined = []
for param in param_list:
tc = cast(TestCaseBase, param.values[0])
for ct in clients:
marks = list(param.marks)
if is_enginex:
assert tc.pre_hash is not None
marks.append(
pytest.mark.xdist_group(
name=make_group_identifier(tc.pre_hash, ct.name)
)
)
combined.append(
pytest.param(
tc,
ct,
id=f"{tc.id}-{ct.name}",
marks=marks,
)
)
metafunc.parametrize("test_case,client_type", combined)
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""Consume hive simulators test functions."""
"""Consume Hive simulators test functions."""
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ def check_live_port(test_suite_name: str) -> Literal[8545, 8551]:
"""Port used by hive to check for liveness of the client."""
if test_suite_name == "eels/consume-rlp":
return 8545
elif test_suite_name in {"eels/consume-engine", "eels/consume-sync"}:
elif test_suite_name in {
"eels/consume-engine",
"eels/consume-enginex",
"eels/consume-sync",
}:
return 8551
raise ValueError(
f"Unexpected test suite name '{test_suite_name}' while setting "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@
from typing import Mapping

import pytest
from hive.client import Client

from execution_testing.exceptions import ExceptionMapper
from execution_testing.fixtures import BlockchainEngineFixture
from execution_testing.rpc import EngineRPC
from execution_testing.fixtures.blockchain import FixtureHeader

pytest_plugins = (
"execution_testing.cli.pytest_commands.plugins.pytest_hive.pytest_hive",
Expand All @@ -21,6 +19,7 @@
"execution_testing.cli.pytest_commands.plugins.consume.simulators.test_case_description",
"execution_testing.cli.pytest_commands.plugins.consume.simulators.timing_data",
"execution_testing.cli.pytest_commands.plugins.consume.simulators.exceptions",
"execution_testing.cli.pytest_commands.plugins.consume.simulators.engine_api",
)


Expand All @@ -29,21 +28,6 @@ def pytest_configure(config: pytest.Config) -> None:
config.supported_fixture_formats = [BlockchainEngineFixture] # type: ignore[attr-defined]


@pytest.fixture(scope="function")
def engine_rpc(
client: Client, client_exception_mapper: ExceptionMapper | None
) -> EngineRPC:
"""Initialize engine RPC client for the execution client under test."""
if client_exception_mapper:
return EngineRPC(
f"http://{client.ip}:8551",
response_validation_context={
"exception_mapper": client_exception_mapper,
},
)
return EngineRPC(f"http://{client.ip}:8551")


@pytest.fixture(scope="module")
def test_suite_name() -> str:
"""The name of the hive test suite used in this simulator."""
Expand All @@ -64,3 +48,9 @@ def client_files(
files = {}
files["/genesis.json"] = buffered_genesis
return files


@pytest.fixture(scope="function")
def genesis_header(fixture: BlockchainEngineFixture) -> "FixtureHeader":
"""Provide the genesis header from the fixture."""
return fixture.genesis
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Pytest fixtures for Engine API RPC clients."""

import pytest
from hive.client import Client

from execution_testing.exceptions import ExceptionMapper
from execution_testing.rpc import EngineRPC


@pytest.fixture(scope="function")
def engine_rpc(
client: Client, client_exception_mapper: ExceptionMapper | None
) -> EngineRPC:
"""
Initialize Engine RPC client for the execution client under test.

Provide a configured EngineRPC instance that communicates
with the client's Engine API endpoint (port 8551). If an
exception mapper is available, it will be used for response
validation to map client-specific error messages to standard
exception types.

Args:
client: The Hive client instance to connect to.
client_exception_mapper: Optional exception mapper.

Returns:
Configured EngineRPC instance for making Engine API calls.

"""
if client_exception_mapper:
return EngineRPC(
f"http://{client.ip}:8551",
response_validation_context={
"exception_mapper": client_exception_mapper,
},
)
return EngineRPC(f"http://{client.ip}:8551")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Engine X simulator for `blockchain_test_engine_x` fixtures."""
Loading