From 90ad3b66e100790557d6c35da8a37c8c0aff83dd Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 5 Aug 2026 12:23:05 +0200 Subject: [PATCH 1/2] fix(test-fixtures): gate target gas limit on forkchoice updated v4 --- .../testing/src/execution_testing/fixtures/blockchain.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/testing/src/execution_testing/fixtures/blockchain.py b/packages/testing/src/execution_testing/fixtures/blockchain.py index 5ad3d932fa..a733137878 100644 --- a/packages/testing/src/execution_testing/fixtures/blockchain.py +++ b/packages/testing/src/execution_testing/fixtures/blockchain.py @@ -561,7 +561,13 @@ def get_payload_attributes(self) -> "PayloadAttributes": withdrawals=execution_payload.withdrawals, parent_beacon_block_root=parent_beacon_block_root, slot_number=execution_payload.slot_number, - target_gas_limit=execution_payload.gas_limit, + # targetGasLimit exists from V4 onwards; earlier versions must + # not carry the field even though every payload has a gas limit. + target_gas_limit=( + execution_payload.gas_limit + if self.forkchoice_updated_version >= 4 + else None + ), ) @staticmethod From 5dfe54523acada9ed87b55b2410b1c0a1db6c4c3 Mon Sep 17 00:00:00 2001 From: spencer-tb Date: Wed, 5 Aug 2026 12:23:05 +0200 Subject: [PATCH 2/2] feat(testing): add engine payload attribute and genesis parity tests --- .../cli/tests/test_execute_genesis.py | 48 ++++++++ .../rpc/tests/test_payload_attributes.py | 115 ++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 packages/testing/src/execution_testing/cli/tests/test_execute_genesis.py create mode 100644 packages/testing/src/execution_testing/rpc/tests/test_payload_attributes.py diff --git a/packages/testing/src/execution_testing/cli/tests/test_execute_genesis.py b/packages/testing/src/execution_testing/cli/tests/test_execute_genesis.py new file mode 100644 index 0000000000..8205788375 --- /dev/null +++ b/packages/testing/src/execution_testing/cli/tests/test_execute_genesis.py @@ -0,0 +1,48 @@ +""" +Test the execute genesis header against the fill genesis header. + +``execute``/``fill-stateful`` build the client genesis with +``build_genesis_header`` while ``fill`` uses ``FixtureHeader.genesis``; +both hand-populate fork-conditional header fields, so a fork that +extends the header must extend both. Feeding them equivalent inputs and +comparing the result catches one-sided drift. +""" + +from typing import Any, Dict, List + +import pytest + +from execution_testing.base_types import to_json +from execution_testing.cli.pytest_commands.plugins.execute.rpc.hive import ( + build_genesis_header, +) +from execution_testing.fixtures.blockchain import FixtureHeader +from execution_testing.forks import ( + Fork, + get_deployed_forks, + get_development_forks, +) +from execution_testing.specs.blockchain import GENESIS_ENVIRONMENT_DEFAULTS +from execution_testing.test_types import Environment + +# ``build_genesis_header`` pins these two values instead of taking them +# from the environment; mirror them so both builders receive equivalent +# inputs and any difference is a structural one. +EXECUTE_GENESIS_ENVIRONMENT: Dict[str, Any] = GENESIS_ENVIRONMENT_DEFAULTS | { + "timestamp": 1, + "difficulty": 0x20000, +} + +FORKS: List[Fork] = get_deployed_forks() + get_development_forks() + + +@pytest.mark.parametrize("fork", FORKS, ids=lambda fork: fork.name()) +def test_execute_genesis_matches_fill_genesis(fork: Fork) -> None: + """The two genesis builders must produce identical headers.""" + pre_alloc, execute_genesis = build_genesis_header(fork) + env = Environment(**EXECUTE_GENESIS_ENVIRONMENT).set_fork_requirements( + fork + ) + fill_genesis = FixtureHeader.genesis(fork, env, pre_alloc.state_root()) + assert to_json(execute_genesis) == to_json(fill_genesis) + assert execute_genesis.block_hash == fill_genesis.block_hash diff --git a/packages/testing/src/execution_testing/rpc/tests/test_payload_attributes.py b/packages/testing/src/execution_testing/rpc/tests/test_payload_attributes.py new file mode 100644 index 0000000000..a9ce47d4e4 --- /dev/null +++ b/packages/testing/src/execution_testing/rpc/tests/test_payload_attributes.py @@ -0,0 +1,115 @@ +""" +Test fork-aware construction of engine API payload attributes. + +Every ``engine_payload_attribute_*`` fork predicate must be honored by +both ``PayloadAttributes`` producers: ``PayloadAttributes.for_fork`` +(used by ``execute`` and ``fill-stateful`` to build blocks live) and +``FixtureEngineNewPayload.get_payload_attributes`` (used by ``consume`` +to have a client build fixture blocks). A fork that adds a payload +attribute fails these tests until both producers populate it. +""" + +from typing import List + +import pytest + +from execution_testing.base_types import Hash +from execution_testing.fixtures.blockchain import ( + FixtureEngineNewPayload, + FixtureHeader, +) +from execution_testing.forks import ( + Fork, + get_deployed_forks, + get_development_forks, +) +from execution_testing.rpc.rpc_types import PayloadAttributes +from execution_testing.specs.blockchain import GENESIS_ENVIRONMENT_DEFAULTS +from execution_testing.test_types import BlockAccessList, Environment + +ENGINE_PAYLOAD_ATTRIBUTE_PREFIX = "engine_payload_attribute_" + +ALL_FORKS: List[Fork] = get_deployed_forks() + get_development_forks() +ENGINE_FORKS: List[Fork] = [ + fork + for fork in ALL_FORKS + if fork.engine_forkchoice_updated_version() is not None +] + + +def engine_payload_attribute_predicates(fork: Fork) -> List[str]: + """Return the fork's engine payload attribute predicate names.""" + return [ + name + for name in dir(fork) + if name.startswith(ENGINE_PAYLOAD_ATTRIBUTE_PREFIX) + ] + + +def assert_attributes_cover_fork( + attributes: PayloadAttributes, fork: Fork +) -> None: + """ + Assert every predicate-gated payload attribute is populated exactly + when the fork requires it. + """ + for predicate_name in engine_payload_attribute_predicates(fork): + field = predicate_name.removeprefix(ENGINE_PAYLOAD_ATTRIBUTE_PREFIX) + assert field in PayloadAttributes.model_fields, ( + f"{predicate_name} has no matching `{field}` field on " + "PayloadAttributes" + ) + required = getattr(fork, predicate_name)() + populated = getattr(attributes, field) is not None + assert populated == required, ( + f"`{field}` must be {'set' if required else 'unset'} for {fork}" + ) + + +def test_predicate_discovery() -> None: + """The predicate discovery must find the known predicate family.""" + names = engine_payload_attribute_predicates(ALL_FORKS[0]) + assert f"{ENGINE_PAYLOAD_ATTRIBUTE_PREFIX}slot_number" in names + + +@pytest.mark.parametrize("fork", ALL_FORKS, ids=lambda fork: fork.name()) +def test_for_fork_covers_every_engine_payload_attribute(fork: Fork) -> None: + """``for_fork`` must populate every attribute the fork requires.""" + attributes = PayloadAttributes.for_fork( + fork, + timestamp=2, + target_gas_limit=30_000_000, + slot_number=7, + ) + assert_attributes_cover_fork(attributes, fork) + if fork.engine_payload_attribute_slot_number(): + assert attributes.slot_number == 7 + if fork.engine_payload_attribute_target_gas_limit(): + assert attributes.target_gas_limit == 30_000_000 + + +@pytest.mark.parametrize("fork", ENGINE_FORKS, ids=lambda fork: fork.name()) +def test_fixture_payload_covers_every_engine_payload_attribute( + fork: Fork, +) -> None: + """ + Attributes rebuilt from a fixture payload must populate every + attribute the fork requires. + """ + env = Environment(**GENESIS_ENVIRONMENT_DEFAULTS).set_fork_requirements( + fork + ) + header = FixtureHeader.genesis(fork, env, Hash(0)) + payload = FixtureEngineNewPayload.from_fixture_header( + fork=fork, + header=header, + transactions=[], + withdrawals=[] if fork.header_withdrawals_required() else None, + requests=[] if fork.engine_new_payload_requests() else None, + block_access_list=( + BlockAccessList().rlp + if fork.engine_execution_payload_block_access_list() + else None + ), + ) + assert_attributes_cover_fork(payload.get_payload_attributes(), fork)