Skip to content

feat(consume): Add consume sync tests to sync clients after engine tests #2007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 12, 2025
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
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Users can select any of the artifacts depending on their testing needs for their
- ✨ Added support for the `--benchmark-gas-values` flag in the `fill` command, allowing a single genesis file to be used across different gas limit settings when generating fixtures. ([#1895](https://github.com/ethereum/execution-spec-tests/pull/1895)).
- ✨ Static tests can now specify a maximum fork where they should be filled for ([#1977](https://github.com/ethereum/execution-spec-tests/pull/1977)).
- ✨ Static tests can now be filled in every format using `--generate-all-formats` ([#2006](https://github.com/ethereum/execution-spec-tests/pull/2006)).
- ✨ Add support for `BlockchainEngineSyncFixture` format for tests marked with `pytest.mark.verify_sync` to enable client synchronization testing via `consume sync` command ([#2007](https://github.com/ethereum/execution-spec-tests/pull/2007)).

#### `consume`

Expand All @@ -90,6 +91,7 @@ Users can select any of the artifacts depending on their testing needs for their
- 🔀 `consume` now automatically avoids GitHub API calls when using direct release URLs (better for CI environments), while release specifiers like `stable@latest` continue to use the API for version resolution ([#1788](https://github.com/ethereum/execution-spec-tests/pull/1788)).
- 🔀 Refactor consume simulator architecture to use explicit pytest plugin structure with forward-looking architecture ([#1801](https://github.com/ethereum/execution-spec-tests/pull/1801)).
- 🔀 Add exponential retry logic to initial fcu within consume engine ([#1815](https://github.com/ethereum/execution-spec-tests/pull/1815)).
- ✨ Add `consume sync` command to test client synchronization capabilities by having one client sync from another via Engine API and P2P networking ([#2007](https://github.com/ethereum/execution-spec-tests/pull/2007)).

#### `execute`

Expand Down
20 changes: 20 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 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 |
| [`execute remote`](./execute/remote.md) | Tests executed against a client via JSON RPC `eth_sendRawTransaction` on a live network | EVM, JSON RPC, mempool, EL-EL/EL-CL interaction (indirectly) | Production | System Test |
Expand Down Expand Up @@ -81,6 +82,25 @@ The `consume rlp` command:

This method simulates how clients import blocks during historical sync, testing the complete block validation and state transition pipeline, see below for more details and a comparison to consumption via the Engine API.

## Sync

| Nomenclature | |
| -------------- |------------------------|
| Command | `consume sync` |
| Simulator | None |
| Fixture format | `blockchain_test_sync` |

The consume sync method tests execution client synchronization capabilities by having one client sync from another via the Engine API and P2P networking. This method validates that clients can correctly synchronize state and blocks from peers, testing both the Engine API, sync triggering, and P2P block propagation mechanisms.

The `consume sync` command:

1. **Initializes the client under test** with genesis state and executes all test payloads.
2. **Spins up a sync client** with the same genesis state.
3. **Establishes P2P connection** between the two clients, utilizing ``admin_addPeer`` with enode url.
4. **Triggers synchronization** by sending the target block to the sync client via `engine_newPayload` followed by `engine_forkchoiceUpdated` requests.
5. **Monitors sync progress** and validates that the sync client reaches the same state.
6. **Verifies final state** matches between both clients.

## Engine vs RLP Simulator

The RLP Simulator (`eest/consume-rlp`) and the Engine Simulator (`eest/consume-engine`) should be seen as complimentary to one another. Although they execute the same underlying EVM test cases, the block validation logic is executed via different client code paths (using different [fixture formats](./test_formats/index.md)). Therefore, ideally, **both simulators should be executed for full coverage**.
Expand Down
239 changes: 239 additions & 0 deletions docs/running_tests/test_formats/blockchain_test_sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
# Blockchain Engine Sync Tests <!-- markdownlint-disable MD051 (MD051=link-fragments "Link fragments should be valid") -->

The Blockchain Engine Sync Test fixture format tests are included in the fixtures subdirectory `blockchain_tests_sync`, and use Engine API directives to test client synchronization capabilities after fixtures are executed with valid payloads.

These are produced by the `BlockchainTest` test spec when `pytest.mark.verify_sync` is used as a test marker.

## Description

The Blockchain Engine Sync Test fixture format is used to test execution client synchronization between peers. It validates that clients can correctly sync state and blocks from another client using the Engine API and P2P networking.

The test works by:

1. Setting up a client under test, defining a pre-execution state, a series of `engine_newPayloadVX` directives, and a post-execution state, as in Blockchain Engine Test fixture formats.
2. Starting a sync client with the same genesis and pre-execution state.
3. Having the sync client synchronize from the client under test.
4. Verifying that both clients reach the same final state.

A single JSON fixture file is composed of a JSON object where each key-value pair is a different [`SyncFixture`](#syncfixture) test object, with the key string representing the test name.

The JSON file path plus the test name are used as the unique test identifier, as well as a `{client under test}::sync_{sync client}` identifier.

## Consumption

For each [`HiveFixture`](#hivefixture) test object in the JSON fixture file, perform the following steps:

### Client Under Test Setup

1. Start the client under test using:
- [`network`](#-network-fork) to configure the execution fork schedule according to the [`Fork`](./common_types.md#fork) type definition.
- [`pre`](#-pre-alloc) as the starting state allocation of the execution environment for the test and calculate the genesis state root.
- [`genesisBlockHeader`](#-genesisblockheader-fixtureheader) as the genesis block header.

2. Verify the head of the chain is the genesis block, and the state root matches the one calculated on step 1, otherwise fail the test.

3. Process all [`FixtureEngineNewPayload`](#fixtureenginenewpayload) objects in [`engineNewPayloads`](#-enginenewpayloads-listfixtureenginenewpayload) to build the complete chain on the client under test.

### Sync Client Setup and Synchronization

1. Start a sync client using the same genesis configuration:
- Use the same [`network`](#-network-fork), [`pre`](#-pre-alloc), and [`genesisBlockHeader`](#-genesisblockheader-fixtureheader).

2. Establish P2P connection between the clients:
- Get the enode URL from the client under test
- Use `admin_addPeer` to connect the sync client to the client under test

3. Trigger synchronization on the sync client:
- Send the [`syncPayload`](#-syncpayload-fixtureenginenewpayload) using `engine_newPayloadVX`
- Send `engine_forkchoiceUpdatedVX` pointing to the last block hash

4. Monitor and verify synchronization:
- Wait for the sync client to reach the [`lastblockhash`](#-lastblockhash-hash)
- Verify the final state root matches between both clients
- If [`post`](#-post-alloc) is provided, verify the final state matches

## Structures

### `HiveFixture`

#### - `network`: [`Fork`](./common_types.md#fork)

##### TO BE DEPRECATED

Fork configuration for the test.

This field is going to be replaced by the value contained in `config.network`.

#### - `genesisBlockHeader`: [`FixtureHeader`](./blockchain_test.md#fixtureheader)

Genesis block header.

#### - `engineNewPayloads`: [`List`](./common_types.md#list)`[`[`FixtureEngineNewPayload`](#fixtureenginenewpayload)`]`

List of `engine_newPayloadVX` directives to be processed by the client under test to build the complete chain.

#### - `syncPayload`: [`FixtureEngineNewPayload`](#fixtureenginenewpayload)

The final payload to be sent to the sync client to trigger synchronization. This is typically an empty block built on top of the last test block.

#### - `engineFcuVersion`: [`Number`](./common_types.md#number)

Version of the `engine_forkchoiceUpdatedVX` directive to use to set the head of the chain.

#### - `pre`: [`Alloc`](./common_types.md#alloc-mappingaddressaccount)

Starting account allocation for the test. State root calculated from this allocation must match the one in the genesis block.

#### - `lastblockhash`: [`Hash`](./common_types.md#hash)

Hash of the last valid block that the sync client should reach after successful synchronization.

#### - `post`: [`Alloc`](./common_types.md#alloc-mappingaddressaccount)

Account allocation for verification after synchronization is complete.

#### - `postStateHash`: [`Optional`](./common_types.md#optional)`[`[`Hash`](./common_types.md#hash)`]`

Optional state root hash for verification after synchronization is complete. Used when full post-state is not included.

#### - `config`: [`FixtureConfig`](#fixtureconfig)

Chain configuration object to be applied to both clients running the blockchain sync test.

### `FixtureConfig`

#### - `network`: [`Fork`](./common_types.md#fork)

Fork configuration for the test. It is guaranteed that this field contains the same value as the root field `network`.

#### - `chainId`: [`Number`](./common_types.md#number)

Chain ID configuration for the test network.

#### - `blobSchedule`: [`BlobSchedule`](./common_types.md#blobschedule-mappingforkforkblobschedule)

Optional; present from Cancun on. Maps forks to their blob schedule configurations as defined by [EIP-7840](https://eips.ethereum.org/EIPS/eip-7840).

### `FixtureEngineNewPayload`

#### - `executionPayload`: [`FixtureExecutionPayload`](#fixtureexecutionpayload)

Execution payload.

#### - `blob_versioned_hashes`: [`Optional`](./common_types.md#optional)`[`[`List`](./common_types.md#list)`[`[`Hash`](./common_types.md#hash)`]]` `(fork: Cancun)`

List of hashes of the versioned blobs that are part of the execution payload.

#### - `parentBeaconBlockRoot`: [`Optional`](./common_types.md#optional)`[`[`Hash`](./common_types.md#hash)`]` `(fork: Cancun)`

Hash of the parent beacon block root.

#### - `validationError`: [`Optional`](./common_types.md#optional)`[`[`TransactionException`](../../library/ethereum_test_exceptions.md#ethereum_test_exceptions.TransactionException)` | `[`BlockException`](../../library/ethereum_test_exceptions.md#ethereum_test_exceptions.BlockException)`]`

For sync tests, this field should not be present as sync tests only work with valid chains. Invalid blocks cannot be synced.

#### - `version`: [`Number`](./common_types.md#number)

Version of the `engine_newPayloadVX` directive to use to deliver the payload.

### `FixtureExecutionPayload`

#### - `parentHash`: [`Hash`](./common_types.md#hash)

Hash of the parent block.

#### - `feeRecipient`: [`Address`](./common_types.md#address)

Address of the account that will receive the rewards for building the block.

#### - `stateRoot`: [`Hash`](./common_types.md#hash)

Root hash of the state trie.

#### - `receiptsRoot`: [`Hash`](./common_types.md#hash)

Root hash of the receipts trie.

#### - `logsBloom`: [`Bloom`](./common_types.md#bloom)

Bloom filter composed of the logs of all the transactions in the block.

#### - `blockNumber`: [`HexNumber`](./common_types.md#hexnumber)

Number of the block.

#### - `gasLimit`: [`HexNumber`](./common_types.md#hexnumber)

Total gas limit of the block.

#### - `gasUsed`: [`HexNumber`](./common_types.md#hexnumber)

Total gas used by all the transactions in the block.

#### - `timestamp`: [`HexNumber`](./common_types.md#hexnumber)

Timestamp of the block.

#### - `extraData`: [`Bytes`](./common_types.md#bytes)

Extra data of the block.

#### - `prevRandao`: [`Hash`](./common_types.md#hash)

PrevRandao of the block.

#### - `blockHash`: [`Hash`](./common_types.md#hash)

Hash of the block.

#### - `transactions`: [`List`](./common_types.md#list)`[`[`Bytes`](./common_types.md#bytes)`]`

List of transactions in the block, in serialized format.

#### - `withdrawals`: [`List`](./common_types.md#list)`[`[`FixtureWithdrawal`](#fixturewithdrawal)`]`

List of withdrawals in the block.

#### - `baseFeePerGas`: [`HexNumber`](./common_types.md#hexnumber) `(fork: London)`

Base fee per gas of the block.

#### - `blobGasUsed`: [`HexNumber`](./common_types.md#hexnumber) `(fork: Cancun)`

Total blob gas used by all the transactions in the block.

#### - `excessBlobGas`: [`HexNumber`](./common_types.md#hexnumber) `(fork: Cancun)`

Excess blob gas of the block used to calculate the blob fee per gas for this block.

### `FixtureWithdrawal`

#### - `index`: [`HexNumber`](./common_types.md#hexnumber)

Index of the withdrawal

#### - `validatorIndex`: [`HexNumber`](./common_types.md#hexnumber)

Withdrawing validator index

#### - `address`: [`Address`](./common_types.md#address)

Address to withdraw to

#### - `amount`: [`HexNumber`](./common_types.md#hexnumber)

Amount of the withdrawal

## Differences from Blockchain Engine Tests

While the Blockchain Sync Test format is similar to the Blockchain Engine Test format, there are key differences:

1. **`syncPayload` field**: Contains the final block used to trigger synchronization on the sync client.
2. **Multi-client testing**: Tests involve two clients (client under test and sync client) rather than a single client.
3. **P2P networking**: Tests require P2P connection establishment between clients.
4. **No invalid blocks**: Sync tests only work with valid chains as invalid blocks cannot be synced.
5. **`postStateHash` field**: Optional field for state verification when full post-state is not included.

## Fork Support

Blockchain Sync Tests are only supported for post-merge forks (Paris and later) as they rely on the Engine API for synchronization triggering.
10 changes: 10 additions & 0 deletions src/cli/pytest_commands/consume.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def get_command_logic_test_paths(command_name: str, is_hive: bool) -> List[Path]
command_logic_test_paths = [
base_path / "simulators" / "simulator_logic" / f"test_via_{command_name}.py"
]
elif command_name == "sync":
command_logic_test_paths = [
base_path / "simulators" / "simulator_logic" / "test_via_sync.py"
]
elif command_name == "direct":
command_logic_test_paths = [base_path / "direct" / "test_via_direct.py"]
else:
Expand Down Expand Up @@ -107,6 +111,12 @@ def engine() -> None:
pass


@consume_command(is_hive=True)
def sync() -> None:
"""Client consumes via the Engine API with sync testing."""
pass


@consume_command(is_hive=True)
def hive() -> None:
"""Client consumes via all available hive methods (rlp, engine)."""
Expand Down
2 changes: 2 additions & 0 deletions src/cli/pytest_commands/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ def process_args(self, args: List[str]) -> List[str]:

if self.command_name == "engine":
modified_args.extend(["-p", "pytest_plugins.consume.simulators.engine.conftest"])
elif self.command_name == "sync":
modified_args.extend(["-p", "pytest_plugins.consume.simulators.sync.conftest"])
elif self.command_name == "rlp":
modified_args.extend(["-p", "pytest_plugins.consume.simulators.rlp.conftest"])
else:
Expand Down
2 changes: 2 additions & 0 deletions src/ethereum_test_fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .blockchain import (
BlockchainEngineFixture,
BlockchainEngineFixtureCommon,
BlockchainEngineSyncFixture,
BlockchainEngineXFixture,
BlockchainFixture,
BlockchainFixtureCommon,
Expand All @@ -19,6 +20,7 @@
"BaseFixture",
"BlockchainEngineFixture",
"BlockchainEngineFixtureCommon",
"BlockchainEngineSyncFixture",
"BlockchainEngineXFixture",
"BlockchainFixture",
"BlockchainFixtureCommon",
Expand Down
28 changes: 26 additions & 2 deletions src/ethereum_test_fixtures/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)

import ethereum_rlp as eth_rlp
import pytest
from ethereum_types.numeric import Uint
from pydantic import AliasChoices, Field, PlainSerializer, computed_field, model_validator

Expand Down Expand Up @@ -543,7 +544,6 @@ class BlockchainEngineFixture(BlockchainEngineFixtureCommon):
genesis: FixtureHeader = Field(..., alias="genesisBlockHeader")
post_state: Alloc | None = Field(None)
payloads: List[FixtureEngineNewPayload] = Field(..., alias="engineNewPayloads")
sync_payload: FixtureEngineNewPayload | None = None


@post_state_validator(alternate_field="post_state_diff")
Expand Down Expand Up @@ -571,5 +571,29 @@ class BlockchainEngineXFixture(BlockchainEngineFixtureCommon):
payloads: List[FixtureEngineNewPayload] = Field(..., alias="engineNewPayloads")
"""Engine API payloads for blockchain execution."""


class BlockchainEngineSyncFixture(BlockchainEngineFixture):
"""
Engine Sync specific test fixture information.

This fixture format is specifically designed for sync testing where:
- The client under test receives all payloads
- A sync client attempts to sync from the client under test
- Both client types are parametrized from hive client config
"""

format_name: ClassVar[str] = "blockchain_test_sync"
description: ClassVar[str] = (
"Tests that generate a blockchain test fixture for Engine API testing with client sync."
)
sync_payload: FixtureEngineNewPayload | None = None
"""Optional sync payload for blockchain synchronization."""

@classmethod
def discard_fixture_format_by_marks(
cls,
fork: Fork,
markers: List[pytest.Mark],
) -> bool:
"""Discard the fixture format based on the provided markers."""
marker_names = [m.name for m in markers]
return "verify_sync" not in marker_names
Loading
Loading