Skip to content

feat(tools,forks): Extend EEST to support EIP-7928 payload #1866

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

Draft
wants to merge 9 commits into
base: feat/block-access-list
Choose a base branch
from
Draft
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
20 changes: 20 additions & 0 deletions src/ethereum_test_fixtures/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class FixtureHeader(CamelModel):
None
)
requests_hash: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None)
bal_hash: Annotated[Hash, HeaderForkRequirement("bal_hash")] | None = Field(None)

fork: Fork | None = Field(None, exclude=True)

Expand Down Expand Up @@ -243,6 +244,7 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> "FixtureHead
),
parent_beacon_block_root=env.parent_beacon_block_root,
requests_hash=Requests() if fork.header_requests_required(0, 0) else None,
bal_hash=Hash(0) if fork.header_bal_hash_required(0, 0) else None,
fork=fork,
)

Expand Down Expand Up @@ -272,6 +274,7 @@ class FixtureExecutionPayload(CamelModel):

transactions: List[Bytes]
withdrawals: List[Withdrawal] | None = None
block_access_list: Bytes | None = Field(None)

@classmethod
def from_fixture_header(
Expand Down Expand Up @@ -300,9 +303,18 @@ def from_fixture_header(
List[Bytes],
]

EngineNewPayloadV5Parameters = Tuple[
FixtureExecutionPayload,
List[Hash],
Hash,
List[Bytes],
Bytes, # block_access_list
]

# Important: We check EngineNewPayloadV3Parameters first as it has more fields, and pydantic
# has a weird behavior when the smaller tuple is checked first.
EngineNewPayloadParameters = Union[
EngineNewPayloadV5Parameters,
EngineNewPayloadV4Parameters,
EngineNewPayloadV3Parameters,
EngineNewPayloadV1Parameters,
Expand Down Expand Up @@ -342,6 +354,7 @@ def from_fixture_header(
transactions: List[Transaction],
withdrawals: List[Withdrawal] | None,
requests: List[Bytes] | None,
block_access_list: Bytes | None = None,
**kwargs,
) -> "FixtureEngineNewPayload":
"""Create `FixtureEngineNewPayload` from a `FixtureHeader`."""
Expand Down Expand Up @@ -374,6 +387,10 @@ def from_fixture_header(
if requests is None:
raise ValueError(f"Requests are required for ${fork}.")
params.append(requests)
if fork.engine_new_payload_block_access_list(header.number, header.timestamp):
if block_access_list is None:
raise ValueError(f"Block access list is required for ${fork}.")
params.append(block_access_list)

payload_params: EngineNewPayloadParameters = cast(
EngineNewPayloadParameters,
Expand Down Expand Up @@ -420,6 +437,9 @@ class FixtureBlockBase(CamelModel):
txs: List[FixtureTransaction] = Field(default_factory=list, alias="transactions")
ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders")
withdrawals: List[FixtureWithdrawal] | None = None
block_access_lists: Bytes | None = Field(
None, description="Serialized EIP-7928 Block Access Lists"
)

@computed_field(alias="blocknumber") # type: ignore[misc]
@cached_property
Expand Down
14 changes: 14 additions & 0 deletions src/ethereum_test_forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) ->
"""Return true if the header must contain beacon chain requests."""
pass

@classmethod
@abstractmethod
def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool:
"""Return true if the header must contain block access list hash."""
pass

# Gas related abstract methods

@classmethod
Expand Down Expand Up @@ -422,6 +428,14 @@ def engine_new_payload_requests(cls, block_number: int = 0, timestamp: int = 0)
"""Return true if the engine api version requires new payload calls to include requests."""
pass

@classmethod
@abstractmethod
def engine_new_payload_block_access_list(
cls, block_number: int = 0, timestamp: int = 0
) -> bool:
"""Return true if the engine api version requires block access list."""
pass

@classmethod
@abstractmethod
def engine_new_payload_target_blobs_per_block(
Expand Down
30 changes: 30 additions & 0 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,13 @@ def engine_new_payload_requests(cls, block_number: int = 0, timestamp: int = 0)
"""At genesis, payloads do not have requests."""
return False

@classmethod
def engine_new_payload_block_access_list(
cls, block_number: int = 0, timestamp: int = 0
) -> bool:
"""At genesis, payloads do not have block access list."""
return False

@classmethod
def engine_new_payload_target_blobs_per_block(
cls,
Expand Down Expand Up @@ -1473,3 +1480,26 @@ def is_deployed(cls) -> bool:
def solc_min_version(cls) -> Version:
"""Return minimum version of solc that supports this fork."""
return Version.parse("1.0.0") # set a high version; currently unknown


class BlockAccessLists(Prague):
"""A development fork for Block Access Lists."""

@classmethod
def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool:
"""Hash of the block access list is required starting from this fork."""
return True

@classmethod
def engine_new_payload_block_access_list(
cls, block_number: int = 0, timestamp: int = 0
) -> bool:
"""From BlockAccessLists, new payloads include the block access list as a parameter."""
return True

@classmethod
def engine_new_payload_version(
cls, block_number: int = 0, timestamp: int = 0
) -> Optional[int]:
"""From BlockAccessLists, new payload calls must use version 5."""
return 5
17 changes: 17 additions & 0 deletions src/ethereum_test_specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class Header(CamelModel):
excess_blob_gas: Removable | HexNumber | None = None
parent_beacon_block_root: Removable | Hash | None = None
requests_hash: Removable | Hash | None = None
bal_hash: Removable | Hash | None = None

REMOVE_FIELD: ClassVar[Removable] = Removable()
"""
Expand Down Expand Up @@ -240,6 +241,10 @@ class Block(Header):
"""
Post state for verification after block execution in BlockchainTest
"""
block_access_lists: Bytes | None = Field(None)
"""
EIP-7928: Block-level access lists (serialized).
"""

def set_environment(self, env: Environment) -> Environment:
"""
Expand Down Expand Up @@ -269,6 +274,14 @@ def set_environment(self, env: Environment) -> Environment:
new_env_values["blob_gas_used"] = self.blob_gas_used
if not isinstance(self.parent_beacon_block_root, Removable):
new_env_values["parent_beacon_block_root"] = self.parent_beacon_block_root
if not isinstance(self.requests_hash, Removable) and self.block_access_lists is not None:
new_env_values["bal_hash"] = self.block_access_lists.keccak256()
new_env_values["block_access_lists"] = self.block_access_lists
if (
not isinstance(self.block_access_lists, Removable)
and self.block_access_lists is not None
):
new_env_values["block_access_lists"] = self.block_access_lists
"""
These values are required, but they depend on the previous environment,
so they can be calculated here.
Expand Down Expand Up @@ -308,6 +321,7 @@ class BuiltBlock(CamelModel):
expected_exception: BLOCK_EXCEPTION_TYPE = None
engine_api_error_code: EngineAPIError | None = None
fork: Fork
block_access_lists: Bytes

def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock:
"""Get a FixtureBlockBase from the built block."""
Expand All @@ -319,6 +333,7 @@ def get_fixture_block(self) -> FixtureBlock | InvalidFixtureBlock:
if self.withdrawals is not None
else None
),
block_access_lists=self.block_access_lists,
fork=self.fork,
).with_rlp(txs=self.txs)

Expand Down Expand Up @@ -347,6 +362,7 @@ def get_fixture_engine_new_payload(self) -> FixtureEngineNewPayload:
transactions=self.txs,
withdrawals=self.withdrawals,
requests=self.requests,
block_access_list=self.block_access_lists,
validation_error=self.expected_exception,
error_code=self.engine_api_error_code,
)
Expand Down Expand Up @@ -574,6 +590,7 @@ def generate_block_data(
expected_exception=block.exception,
engine_api_error_code=block.engine_api_error_code,
fork=fork,
block_access_lists=block.block_access_lists,
)

try:
Expand Down
4 changes: 4 additions & 0 deletions src/ethereum_test_types/block_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ class Environment(EnvironmentGeneric[ZeroPaddedHexNumber]):
withdrawals: List[Withdrawal] | None = Field(None)
extra_data: Bytes = Field(Bytes(b"\x00"), exclude=True)

# EIP-7928: Block-level access lists
bal_hash: Hash | None = Field(None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this belongs to theResult returned from the transition tool after executing the state transition.

Environment is more for things that affect the EVM execution and/or are observable in the EVM context.

If we put it in Result, we can catch it in here:

header = FixtureHeader(
**(
transition_tool_output.result.model_dump(
exclude_none=True, exclude={"blob_gas_used", "transactions_trie"}
)
| env.model_dump(exclude_none=True, exclude={"blob_gas_used"})
),
blob_gas_used=blob_gas_used,
transactions_trie=Transaction.list_root(txs),
extra_data=block.extra_data if block.extra_data is not None else b"",
fork=fork,
)

and then even verify it during the test by using header_verify as in this example here.

Copy link
Member Author

@raxhvl raxhvl Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @marioevz I see this differently. The test supplies the following to the client as part of a block:

  1. A list of transactions
  2. SSZ encoded block access list (BAL)
  3. BAL hash

In the PR (1) and (2) is part of a test. (3) is computed by the framework from (2).

State transition

The client's state transition function takes all three to produce a block. In addition to executing the transactions, the client:

  1. Computes its own copy BAL and then
  2. Computes BAL hash

The Client and NOT test verifies hash

The client MUST reject the block if computed hash does not match the provided one. ref: spec

If hash is not supplied as an input to the client it will not be able to perform this check. Hence its inclusion in ENV.

block_access_lists: Bytes | None = Field(None)

@computed_field # type: ignore[misc]
@cached_property
def parent_hash(self) -> Hash | None:
Expand Down
3 changes: 3 additions & 0 deletions tests/unscheduled/eip7928_block_level_access_lists/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from dataclasses import dataclass

ACTIVATION_FORK_NAME = "BlockAccessLists"
"""The fork name for EIP-7928 activation."""


@dataclass(frozen=True)
class ReferenceSpec:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,65 @@
"""Tests for validating EIP-7928: Block-level Access Lists (BAL)."""

import pytest
from pokebal.bal.types import BlockAccessList

from ethereum_test_tools import (
Account,
Alloc,
Block,
BlockchainTestFiller,
Transaction,
)

from .spec import ACTIVATION_FORK_NAME, ref_spec_7928

@pytest.mark.valid_from("Amsterdam")
REFERENCE_SPEC_GIT_PATH = ref_spec_7928.git_path
REFERENCE_SPEC_VERSION = ref_spec_7928.version


@pytest.mark.valid_from(ACTIVATION_FORK_NAME)
class TestBALValidity:
"""Test BAL validity and data structure integrity."""

def test_bal_hash_basic_transaction(
def test_eth_transfer(
self,
pre: Alloc,
blockchain_test: BlockchainTestFiller,
):
"""Test BAL hash generation for basic ETH transfer."""
# TODO: Implement BAL hash validation for basic ETH transfer
pass
"""Test ETH transfer and verify balances."""
# Setup accounts for basic ETH transfer
transfer_amount = 1000
sender = pre.fund_eoa()
recipient = pre.fund_eoa(amount=0)

# Create a basic ETH transfer transaction
tx = Transaction(
sender=sender,
to=recipient,
value=transfer_amount,
)

# Create block with the transaction
block = Block(txs=[tx])

# Execute the blockchain test
blockchain_test(
pre=pre,
blocks=[block],
post={},
block_access_list={
"account_changes": [
{
"address": recipient,
"balance_changes": [{"tx_index": 0, "post_balance": transfer_amount}],
},
]
},
)

# Note: In the generated fixture, the block header will include:
# - bal_hash: the computed hash of the Block Access List
# - bal_data: the SSZ-encoded Block Access List data (when framework supports it)

def test_bal_hash_storage_operations(
self,
Expand Down Expand Up @@ -103,7 +143,7 @@ def test_bal_failed_transaction_inclusion(
pass


@pytest.mark.valid_from("Amsterdam")
@pytest.mark.valid_from(ACTIVATION_FORK_NAME)
class TestBALEncoding:
"""Test SSZ encoding/decoding of BAL data structures."""

Expand Down Expand Up @@ -153,7 +193,7 @@ def test_ssz_encoding_full_bal(
pass


@pytest.mark.valid_from("Amsterdam")
@pytest.mark.valid_from(ACTIVATION_FORK_NAME)
class TestBALEdgeCases:
"""Test edge cases and error conditions for BAL."""

Expand Down Expand Up @@ -203,7 +243,7 @@ def test_bal_maximum_block_size(
pass


@pytest.mark.valid_from("Amsterdam")
@pytest.mark.valid_from(ACTIVATION_FORK_NAME)
class TestBALValidationFailures:
"""Test validation failure scenarios for malformed or incorrect BALs."""

Expand Down Expand Up @@ -244,7 +284,7 @@ def test_malformed_ssz_rejection(
pass


@pytest.mark.valid_from("Amsterdam")
@pytest.mark.valid_from(ACTIVATION_FORK_NAME)
class TestBALLimits:
"""Test EIP-7928 specification limits and boundaries."""

Expand Down Expand Up @@ -357,7 +397,7 @@ def test_hash_size_validation(
pass


@pytest.mark.valid_from("Amsterdam")
@pytest.mark.valid_from(ACTIVATION_FORK_NAME)
class TestBALBoundaryConditions:
"""Test boundary conditions and edge cases for EIP-7928 limits."""

Expand Down
Loading
Loading