Skip to content
Open
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
35 changes: 22 additions & 13 deletions packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ class Block(Header):
"""Post state for verification after block execution in BlockchainTest"""
block_access_list: Bytes | None = Field(None)
"""EIP-7928: Block-level access lists (serialized)."""
engine_new_payload_block_access_list: Bytes | None = None
"""EIP-7928: override only the engine newPayload blockAccessList field."""
expected_gas_used: int | None = None
"""Expected gas used for the block."""

Expand Down Expand Up @@ -399,6 +401,7 @@ class BuiltBlock(CamelModel):
rlp_modifier: Header | None = None
fork: Fork
block_access_list: BlockAccessList | None
engine_new_payload_block_access_list: Bytes | None = None

def get_fixture_block(
self, *, include_receipts: bool = True
Expand Down Expand Up @@ -449,10 +452,8 @@ def get_block_rlp(self) -> Bytes:
"""Get the RLP of the block."""
return self.get_fixture_block().rlp

@staticmethod
def derive_engine_payload_modifier(
rlp_modifier: Header | None,
block_access_list: BlockAccessList | None,
def engine_payload_modifier(
self,
) -> "FixtureExecutionPayloadModifier | None":
"""
Propagate ``rlp_modifier``'s header changes to the engine payload.
Expand All @@ -462,9 +463,13 @@ def derive_engine_payload_modifier(
the ``block_access_list`` body. So a header modifier that touches the
BAL hash needs to drive a matching change on the payload body.
"""
if rlp_modifier is None:
if self.engine_new_payload_block_access_list is not None:
return FixtureExecutionPayloadModifier(
block_access_list=self.engine_new_payload_block_access_list,
)
if self.rlp_modifier is None:
return None
bal_hash_override = rlp_modifier.block_access_list_hash
bal_hash_override = self.rlp_modifier.block_access_list_hash
if bal_hash_override is None:
return None
if bal_hash_override is Header.REMOVE_FIELD:
Expand All @@ -477,7 +482,7 @@ def derive_engine_payload_modifier(
# payload by forcing a body to be present. Its exact value is
# irrelevant for negative tests — a non-``None`` value is enough to
# make a payload-version mismatch detectable.
if block_access_list is None:
if self.block_access_list is None:
return FixtureExecutionPayloadModifier(
block_access_list=Bytes(b""),
)
Expand All @@ -494,9 +499,7 @@ def get_fixture_engine_new_payload(self) -> FixtureEngineNewPayload:
block_access_list=self.block_access_list.rlp
if self.block_access_list
else None,
execution_payload_modifier=self.derive_engine_payload_modifier(
self.rlp_modifier, self.block_access_list
),
execution_payload_modifier=self.engine_payload_modifier(),
validation_error=self.expected_exception,
error_code=self.engine_api_error_code,
)
Expand Down Expand Up @@ -850,6 +853,9 @@ def generate_block_data(
rlp_modifier=block.rlp_modifier,
fork=fork,
block_access_list=bal,
engine_new_payload_block_access_list=(
block.engine_new_payload_block_access_list
),
)

try:
Expand All @@ -861,6 +867,7 @@ def generate_block_data(
and block.rlp_modifier is None
and block.requests is None
and not block.skip_exception_verification
and block.engine_new_payload_block_access_list is None
and not (
block.expected_block_access_list is not None
and block.expected_block_access_list._modifier is not None
Expand All @@ -871,9 +878,11 @@ def generate_block_data(
# exceptions. - No RLP modifier was specified, because the
# modifier is what normally produces the block exception. - No
# requests were specified, because modified requests are also
# what normally produces the block exception. - No BAL modifier
# was specified, because modified BAL also produces block
# exceptions.
# what normally produces the block exception. - No engine
# payload BAL override was specified, because it corrupts only
# the engine payload after the transition tool has run. - No
# BAL modifier was specified, because modified BAL also
# produces block exceptions.
built_block.verify_block_exception(
transition_tool_exceptions_reliable=t8n.exception_mapper.reliable,
)
Expand Down
66 changes: 56 additions & 10 deletions packages/testing/src/execution_testing/specs/tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
Hash,
HeaderNonce,
)
from execution_testing.client_clis import Result
from execution_testing.client_clis.cli_types import LazyAllocStr
from execution_testing.fixtures.blockchain import (
FixtureExecutionPayloadModifier,
FixtureHeader,
)
from execution_testing.forks import Amsterdam
from execution_testing.test_types import Environment
from execution_testing.test_types.block_access_list import BlockAccessList

from ..blockchain import BuiltBlock, Header
Expand All @@ -39,6 +43,15 @@
excess_blob_gas=1,
# hash=Hash(1),
)
result_empty = Result(
state_root=0,
transactions_trie=0,
receipts_root=0,
logs_hash=0,
logs_bloom=0,
receipts=[],
gas_used=0,
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -145,6 +158,30 @@ def test_fixture_header_join(
assert modifier.apply(fixture_header) == fixture_header_expected


def built_block(
*,
rlp_modifier: Header | None = None,
block_access_list: BlockAccessList | None = None,
engine_new_payload_block_access_list: Bytes | None = None,
) -> BuiltBlock:
"""Generate a dummy built block with all default values."""
return BuiltBlock(
header=fixture_header_ones,
env=Environment(),
alloc=LazyAllocStr(raw="", _state_root=Hash(0)),
state_root=Hash(0),
txs=[],
ommers=[],
withdrawals=None,
requests=None,
result=result_empty,
fork=Amsterdam,
rlp_modifier=rlp_modifier,
block_access_list=block_access_list,
engine_new_payload_block_access_list=engine_new_payload_block_access_list,
)


class TestDeriveEnginePayloadModifier:
"""
Verify the auto-propagation from ``rlp_modifier``'s header-only changes
Expand All @@ -156,29 +193,30 @@ class TestDeriveEnginePayloadModifier:
def test_no_rlp_modifier_returns_none(self) -> None:
"""No modifier → no engine payload override."""
assert (
BuiltBlock.derive_engine_payload_modifier(
built_block(
rlp_modifier=None,
block_access_list=None,
)
engine_new_payload_block_access_list=None,
).engine_payload_modifier()
is None
)

def test_rlp_modifier_unrelated_field_returns_none(self) -> None:
"""A modifier that doesn't touch BAL hash leaves the payload alone."""
assert (
BuiltBlock.derive_engine_payload_modifier(
built_block(
rlp_modifier=Header(state_root=Hash(100)),
block_access_list=None,
)
).engine_payload_modifier()
is None
)

def test_remove_bal_hash_removes_body_from_payload(self) -> None:
"""Removing the header's BAL hash also removes the payload body."""
modifier = BuiltBlock.derive_engine_payload_modifier(
modifier = built_block(
rlp_modifier=Header(block_access_list_hash=Header.REMOVE_FIELD),
block_access_list=BlockAccessList(),
)
).engine_payload_modifier()
assert isinstance(modifier, FixtureExecutionPayloadModifier)
assert modifier.block_access_list is (
FixtureExecutionPayloadModifier.REMOVE_FIELD
Expand All @@ -190,10 +228,10 @@ def test_inject_bal_hash_on_pre_fork_adds_body(self) -> None:
triggers a body to be added to the engine payload, so a payload-
version mismatch is detectable.
"""
modifier = BuiltBlock.derive_engine_payload_modifier(
modifier = built_block(
rlp_modifier=Header(block_access_list_hash=Hash(0)),
block_access_list=None,
)
).engine_payload_modifier()
assert isinstance(modifier, FixtureExecutionPayloadModifier)
assert modifier.block_access_list == Bytes(b"")

Expand All @@ -204,9 +242,17 @@ def test_inject_bal_hash_on_post_fork_leaves_body_alone(self) -> None:
what triggers the client rejection in that scenario.
"""
assert (
BuiltBlock.derive_engine_payload_modifier(
built_block(
rlp_modifier=Header(block_access_list_hash=Hash(0)),
block_access_list=BlockAccessList(),
)
).engine_payload_modifier()
is None
)

def test_empty_bytes_override_sends_raw_body(self) -> None:
"""Raw `Bytes` (e.g. the invalid `0x`) are sent verbatim."""
modifier = built_block(
engine_new_payload_block_access_list=Bytes(b"")
).engine_payload_modifier()
assert isinstance(modifier, FixtureExecutionPayloadModifier)
assert modifier.block_access_list == Bytes(b"")
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
BlockAccessListExpectation,
BlockchainTestFiller,
BlockException,
Bytes,
EIPChecklist,
EngineAPIError,
Environment,
Fork,
Hash,
Expand Down Expand Up @@ -1627,3 +1629,49 @@ def test_bal_invalid_extraneous_coinbase(
)
],
)


@pytest.mark.valid_from("Amsterdam")
@pytest.mark.blockchain_test_engine_only
@pytest.mark.exception_test
@pytest.mark.parametrize(
"invalid_bal_payload",
[
pytest.param(b"", id="empty_byte_string"),
pytest.param(b"\x80", id="rlp_non_list"),
pytest.param(b"\xc1", id="rlp_truncated_list"),
],
)
def test_bal_invalid_engine_payload_encoding(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
invalid_bal_payload: bytes,
) -> None:
"""
Reject a `newPayload` whose `blockAccessList` does not decode as an RLP
list: the empty byte string `0x` (an empty BAL is `0xc0`), the RLP
empty byte string `0x80` (valid RLP but not a list), or a truncated
list header `0xc1`.
"""
sender = pre.fund_eoa()
receiver = pre.nonexistent_account()

tx = Transaction(sender=sender, to=receiver)

blockchain_test(
pre=pre,
post={
sender: Account(nonce=0),
receiver: None,
},
blocks=[
Block(
txs=[tx],
engine_new_payload_block_access_list=Bytes(
invalid_bal_payload
),
exception=BlockException.INVALID_BLOCK_ACCESS_LIST,
engine_api_error_code=EngineAPIError.InvalidParams,
)
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
BlockAccessListExpectation,
BlockchainTestFiller,
BlockException,
Bytes,
EIPChecklist,
EngineAPIError,
Environment,
Expand Down Expand Up @@ -118,6 +119,42 @@ def test_invalid_pre_fork_block_with_bal_hash_field(
)


@pytest.mark.valid_at_transition_to("Amsterdam")
@pytest.mark.blockchain_test_engine_only
@pytest.mark.exception_test
def test_bal_invalid_engine_payload_field_before_fork(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
) -> None:
"""
Reject a pre-Amsterdam `newPayload` that carries a `blockAccessList`.

The block and its header are otherwise valid, so the spurious payload
field is the only defect: clients that silently drop unknown
`newPayloadV4` fields would answer VALID and must fail this test.
"""
sender = pre.fund_eoa()
receiver = pre.nonexistent_account()

tx = Transaction(sender=sender, to=receiver, value=100)

blockchain_test(
pre=pre,
post={},
blocks=[
Block(
timestamp=FORK_TIMESTAMP - 1,
txs=[tx],
# A valid empty-BAL encoding: field presence alone, not
# decodability, must trigger the rejection.
engine_new_payload_block_access_list=Bytes(b"\xc0"),
exception=BlockException.INCORRECT_BLOCK_FORMAT,
engine_api_error_code=EngineAPIError.InvalidParams,
),
],
)


@EIPChecklist.BlockHeaderField.Test.ForkTransition.After()
@pytest.mark.valid_at_transition_to("Amsterdam")
@pytest.mark.exception_test
Expand Down
Loading