Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
208256f
🚧 wip(EIP-7928): Block-level Access Lists
raxhvl Jul 1, 2025
ab00c0a
✨ feat(EIP-7928): Spec params
raxhvl Jul 1, 2025
3238667
🧹 chore(EIP-7928): Rename package
raxhvl Jul 1, 2025
0c43e3f
✨ feat(EIP-7928): Test cases
raxhvl Jul 1, 2025
b1a9d2f
✨ feat(EIP-7928): Add BAL header
raxhvl Jul 3, 2025
bfaba98
🧹 chore(EIP-7928): bal_hash header
raxhvl Jul 6, 2025
7489c35
🧹 chore: Populate bal_hash env
raxhvl Jul 6, 2025
5dbd10b
✨ feat(EIP-7928): Add BAL to block body
raxhvl Jul 6, 2025
a5059e7
✨ feat(EIP-7928): Add pokebal package
raxhvl Jul 7, 2025
907bb72
✨ feat: update env
raxhvl Jul 28, 2025
674dc25
🚧 wip: Checklist and basic tests
raxhvl Aug 12, 2025
b0c8bd2
🧹 chore: scope
raxhvl Aug 12, 2025
06464af
Add more sophisticated test cases (#2029)
nerolation Aug 12, 2025
d583ef8
WIP: Attempt to get EEST + EELS bals PRs talking to each other
fselmo Aug 20, 2025
43b5549
refactor(bal): Use full bal in t8n; verify and build fixtures
fselmo Aug 21, 2025
6a4e6bb
chore(lint): Fix lint issues, assign method to Frontier to be overridden
fselmo Aug 21, 2025
9bb02f7
fix(tests,lint): Set up code changes test case; fix remaining lint is…
fselmo Aug 21, 2025
d9b2550
refactor(forks): move BAL tests to Amsterdam
fselmo Aug 21, 2025
eb66f67
chore: Fix CI lint issues not present locally
fselmo Aug 21, 2025
1fe330b
refactor(bal): Apply comments from first pass at PR
fselmo Aug 22, 2025
07f4013
refactor(bal): Split BAL into two classes
fselmo Aug 22, 2025
89f38b2
refactor(tests): Rename checklist.md -> test_cases.md for BAL test ca…
fselmo Aug 26, 2025
9eb95ab
feat(tests,tools): [WIP] initial invalid BAL tests
fselmo Aug 26, 2025
95eb661
chore(spec-resolver): Update commit hash to point to for spec resolver
fselmo Aug 27, 2025
f8eaee7
fix(lint,resolver): Use latest commit in resolver; fix lint
fselmo Aug 27, 2025
41f0ed5
Address comments from PR #2067
fselmo Sep 2, 2025
38b6dbf
chore(docs): Add changelog entries for BAL updates
fselmo Sep 2, 2025
c6055af
refactor(types): Refactor BAL checks for explicit exclusion of acct c…
fselmo Sep 2, 2025
d0eb106
fix(bal): Fix explicit empty checks for account changes; add unit tests
fselmo Sep 2, 2025
8570378
feat(bal): Extend BAL support within framework
fselmo Sep 2, 2025
e0c2fba
fix(warnings): Silence warning for unused reference in .md file
fselmo Sep 2, 2025
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
4 changes: 3 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ Users can select any of the artifacts depending on their testing needs for their
- πŸ”€ Move Prague to stable and Osaka to develop ([#1573](https://github.com/ethereum/execution-spec-tests/pull/1573)).
- ✨ Add a `pytest.mark.with_all_typed_transactions` marker that creates default typed transactions for each `tx_type` supported by the current `fork` ([#1890](https://github.com/ethereum/execution-spec-tests/pull/1890)).
- ✨ Add basic support for ``Amsterdam`` fork in order to begin testing Glamsterdam ([#2069](https://github.com/ethereum/execution-spec-tests/pull/2069)).
- ✨ [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928): Add initial framework support for `Block Level Access Lists (BAL)` testing for Amsterdam ([#2067](https://github.com/ethereum/execution-spec-tests/pull/2067)).

### πŸ§ͺ Test Cases

Expand All @@ -154,12 +155,13 @@ Users can select any of the artifacts depending on their testing needs for their
- ✨ [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951): add test cases for `P256VERIFY` precompile to support secp256r1 curve [#1670](https://github.com/ethereum/execution-spec-tests/pull/1670).
- ✨ Introduce blockchain tests for benchmark to cover the scenario of pure ether transfers [#1742](https://github.com/ethereum/execution-spec-tests/pull/1742).
- ✨ [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934): Add test cases for the block RLP max limit of 10MiB ([#1730](https://github.com/ethereum/execution-spec-tests/pull/1730)).
- ✨ [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) Add count leading zeros (CLZ) opcode tests for Osaka ([#1733](https://github.com/ethereum/execution-spec-tests/pull/1733)).
- ✨ [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939): Add count leading zeros (CLZ) opcode tests for Osaka ([#1733](https://github.com/ethereum/execution-spec-tests/pull/1733)).
- ✨ [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934): Add additional test cases for block RLP max limit with all typed transactions and for a log-creating transactions ([#1890](https://github.com/ethereum/execution-spec-tests/pull/1890)).
- ✨ [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825): Pre-Osaka tests have been updated to either (1) dynamically adapt to the transaction gas limit cap, or (2) reduce overall gas consumption to fit the new limit ([#1924](https://github.com/ethereum/EIPs/pull/1924), [#1928](https://github.com/ethereum/EIPs/pull/1928), [#1980](https://github.com/ethereum/EIPs/pull/1980)).
- ✨ [EIP-7918](https://eips.ethereum.org/EIPS/eip-7918): Blob base fee bounded by execution cost test cases (initial), includes some adjustments to [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) tests ([#1685](https://github.com/ethereum/execution-spec-tests/pull/1685)).
- πŸ”€ Adds the max blob transaction limit to the tests including updates to [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) for Osaka ([#1884](https://github.com/ethereum/execution-spec-tests/pull/1884)).
- 🐞 Fix issues when filling block rlp size limit tests with ``--generate-pre-alloc-groups`` ([#1989](https://github.com/ethereum/execution-spec-tests/pull/1989)).
- ✨ [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928): Add test cases for `Block Level Access Lists (BAL)` to Amsterdam ([#2067](https://github.com/ethereum/execution-spec-tests/pull/2067)).

## [v4.5.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.5.0) - 2025-05-14

Expand Down
2 changes: 1 addition & 1 deletion docs/running_tests/test_formats/blockchain_test_sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The test works by:
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.
A single JSON fixture file is composed of a JSON object where each key-value pair is a different [`HiveFixture`](#hivefixture) 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.

Expand Down
10 changes: 9 additions & 1 deletion src/ethereum_clis/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
TransactionException,
UndefinedException,
)
from ethereum_test_types import Alloc, Environment, Transaction, TransactionReceipt
from ethereum_test_types import (
Alloc,
BlockAccessList,
Environment,
Transaction,
TransactionReceipt,
)


class TransactionExceptionWithMessage(ExceptionWithMessage[TransactionException]):
Expand Down Expand Up @@ -57,6 +63,8 @@ class Result(CamelModel):
blob_gas_used: HexNumber | None = None
requests_hash: Hash | None = None
requests: List[Bytes] | None = None
block_access_list: BlockAccessList | None = None
block_access_list_hash: Hash | None = None
block_exception: Annotated[
BlockExceptionWithMessage | UndefinedException | None, ExceptionMapperValidator
] = None
Expand Down
2 changes: 2 additions & 0 deletions src/ethereum_test_base_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
HexNumber,
Number,
NumberBoundTypeVar,
StorageKey,
Wei,
ZeroPaddedHexNumber,
)
Expand Down Expand Up @@ -72,6 +73,7 @@
"RLPSerializable",
"SignableRLPSerializable",
"Storage",
"StorageKey",
"StorageRootType",
"TestAddress",
"TestAddress2",
Expand Down
14 changes: 14 additions & 0 deletions src/ethereum_test_base_types/base_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,20 @@ class Hash(FixedSizeBytes[32]): # type: ignore
pass


class StorageKey(FixedSizeBytes[32]): # type: ignore
"""
Storage key type that automatically applies left padding for values shorter
than 32 bytes.
"""

def __new__(cls, value, **kwargs):
"""Create a new StorageKey with automatic left padding."""
# Always apply left_padding for storage keys unless explicitly set to False
if "left_padding" not in kwargs:
kwargs["left_padding"] = True
return super().__new__(cls, value, **kwargs)


class Bloom(FixedSizeBytes[256]): # type: ignore
"""Class that helps represent blooms in tests."""

Expand Down
20 changes: 19 additions & 1 deletion src/ethereum_test_fixtures/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ethereum_test_exceptions import EngineAPIError, ExceptionInstanceOrList
from ethereum_test_forks import Fork, Paris
from ethereum_test_types import (
BlockAccessList,
Environment,
Requests,
Transaction,
Expand Down Expand Up @@ -165,6 +166,9 @@ class FixtureHeader(CamelModel):
None
)
requests_hash: Annotated[Hash, HeaderForkRequirement("requests")] | None = Field(None)
block_access_list_hash: Annotated[Hash, HeaderForkRequirement("bal_hash")] | None = Field(
None, alias="blockAccessListHash"
)

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

Expand Down Expand Up @@ -231,6 +235,9 @@ def genesis(cls, fork: Fork, env: Environment, state_root: Hash) -> "FixtureHead
extras = {
"state_root": state_root,
"requests_hash": Requests() if fork.header_requests_required(0, 0) else None,
"block_access_list_hash": (
BlockAccessList().rlp_hash if fork.header_bal_hash_required(0, 0) else None
),
"fork": fork,
}
return FixtureHeader(**environment_values, **extras)
Expand Down Expand Up @@ -262,21 +269,27 @@ class FixtureExecutionPayload(CamelModel):
transactions: List[Bytes]
withdrawals: List[Withdrawal] | None = None

block_access_list: Bytes | None = Field(
None, description="RLP-serialized EIP-7928 Block Access List"
)

@classmethod
def from_fixture_header(
cls,
header: FixtureHeader,
transactions: List[Transaction],
withdrawals: List[Withdrawal] | None,
block_access_list: Bytes | None = None,
) -> "FixtureExecutionPayload":
"""
Return FixtureExecutionPayload from a FixtureHeader, a list
of transactions and a list of withdrawals.
of transactions, a list of withdrawals, and an optional block access list.
"""
return cls(
**header.model_dump(exclude={"rlp"}, exclude_none=True),
transactions=[tx.rlp() for tx in transactions],
withdrawals=withdrawals,
block_access_list=block_access_list,
)


Expand Down Expand Up @@ -331,6 +344,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 All @@ -344,6 +358,7 @@ def from_fixture_header(
header=header,
transactions=transactions,
withdrawals=withdrawals,
block_access_list=block_access_list,
)

params: List[Any] = [execution_payload]
Expand Down Expand Up @@ -428,6 +443,9 @@ class FixtureBlockBase(CamelModel):
ommers: List[FixtureHeader] = Field(default_factory=list, alias="uncleHeaders")
withdrawals: List[FixtureWithdrawal] | None = None
execution_witness: WitnessChunk | None = None
block_access_list: Bytes | None = Field(
None, description="Serialized EIP-7928 Block Access List"
)

@computed_field(alias="blocknumber") # type: ignore[misc]
@cached_property
Expand Down
6 changes: 6 additions & 0 deletions src/ethereum_test_forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,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
28 changes: 19 additions & 9 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) ->
"""At genesis, header must not contain beacon chain requests."""
return False

@classmethod
def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool:
"""At genesis, header must not contain block access list hash."""
return False

@classmethod
def engine_new_payload_version(
cls, block_number: int = 0, timestamp: int = 0
Expand Down Expand Up @@ -1808,6 +1813,20 @@ class BPO5(BPO4, bpo_fork=True):
pass


class Amsterdam(Osaka):
"""Amsterdam fork."""

@classmethod
def header_bal_hash_required(cls, block_number: int = 0, timestamp: int = 0) -> bool:
"""From Amsterdam, header must contain block access list hash (EIP-7928)."""
return True

@classmethod
def is_deployed(cls) -> bool:
"""Return True if this fork is deployed."""
return False


class EOFv1(Prague, solc_name="cancun"):
"""EOF fork."""

Expand Down Expand Up @@ -1837,12 +1856,3 @@ def is_deployed(cls) -> bool:
development.
"""
return False


class Amsterdam(Osaka):
"""Amsterdam fork."""

@classmethod
def is_deployed(cls) -> bool:
"""Return True if this fork is deployed."""
return False
65 changes: 59 additions & 6 deletions src/ethereum_test_specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from ethereum_test_fixtures.common import FixtureBlobSchedule
from ethereum_test_forks import Fork
from ethereum_test_types import Alloc, Environment, Removable, Requests, Transaction, Withdrawal
from ethereum_test_types.block_access_list import BlockAccessList, BlockAccessListExpectation

from .base import BaseTest, OpMode, verify_result
from .debugging import print_traces
Expand Down Expand Up @@ -121,6 +122,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 @@ -206,6 +208,10 @@ class Block(Header):
An RLP modifying header which values would be used to override the ones
returned by the `ethereum_clis.TransitionTool`.
"""
expected_block_access_list: BlockAccessListExpectation | None = None
"""
If set, the block access list will be verified and potentially corrupted for invalid tests.
"""
exception: BLOCK_EXCEPTION_TYPE = None
"""
If set, the block is expected to be rejected by the client.
Expand Down Expand Up @@ -240,6 +246,10 @@ 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).
"""

def set_environment(self, env: Environment) -> Environment:
"""
Expand Down Expand Up @@ -269,6 +279,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_list is not None:
new_env_values["bal_hash"] = self.block_access_list.keccak256()
new_env_values["block_access_list"] = self.block_access_list
if (
not isinstance(self.block_access_list, Removable)
and self.block_access_list is not None
):
new_env_values["block_access_list"] = self.block_access_list
"""
These values are required, but they depend on the previous environment,
so they can be calculated here.
Expand Down Expand Up @@ -308,6 +326,7 @@ class BuiltBlock(CamelModel):
expected_exception: BLOCK_EXCEPTION_TYPE = None
engine_api_error_code: EngineAPIError | None = None
fork: Fork
block_access_list: BlockAccessList | None

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

Expand Down Expand Up @@ -349,6 +369,7 @@ def get_fixture_engine_new_payload(self) -> FixtureEngineNewPayload:
requests=self.requests,
validation_error=self.expected_exception,
error_code=self.engine_api_error_code,
block_access_list=self.block_access_list.rlp if self.block_access_list else None,
)

def verify_transactions(self, transition_tool_exceptions_reliable: bool) -> List[int]:
Expand Down Expand Up @@ -585,12 +606,34 @@ def generate_block_data(
header.requests_hash = Hash(Requests(requests_lists=list(block.requests)))
requests_list = block.requests

if fork.header_bal_hash_required(header.number, header.timestamp):
assert transition_tool_output.result.block_access_list is not None, (
"Block access list is required for this block but was not provided "
"by the transition tool"
)

rlp = transition_tool_output.result.block_access_list.rlp
computed_bal_hash = Hash(rlp.keccak256())
assert computed_bal_hash == header.block_access_list_hash, (
"Block access list hash in header does not match the "
f"computed hash from BAL: {header.block_access_list_hash} "
f"!= {computed_bal_hash}"
)

if block.rlp_modifier is not None:
# Modify any parameter specified in the `rlp_modifier` after
# transition tool processing.
header = block.rlp_modifier.apply(header)
header.fork = fork # Deleted during `apply` because `exclude=True`

# Process block access list - apply transformer if present for invalid tests
bal = transition_tool_output.result.block_access_list
if block.expected_block_access_list is not None and bal is not None:
# Use to_fixture_bal to validate and potentially transform the BAL
bal = block.expected_block_access_list.to_fixture_bal(bal)
# Don't update the header hash - leave it as the hash of the correct BAL
# This creates a mismatch that should cause the block to be rejected
Comment on lines +634 to +635
Copy link
Member

Choose a reason for hiding this comment

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

Very nice.

One thing to consider is that when we test this as a Blockchain Engine type of test, potentially the exception is going to change, since we cannot influence the header from the payload we deliver via the Engine API.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm yeah, good point. I'm just now starting to explore this for BAL. Not sure if we want to tackle this further down the line. Should we maybe put together some items to think about for BAL as an issue so we can track them while we iterate and try to get this consolidated for a merge? Let me know.


built_block = BuiltBlock(
header=header,
alloc=transition_tool_output.alloc,
Expand All @@ -603,6 +646,7 @@ def generate_block_data(
expected_exception=block.exception,
engine_api_error_code=block.engine_api_error_code,
fork=fork,
block_access_list=bal,
)

try:
Expand All @@ -614,14 +658,20 @@ def generate_block_data(
and block.rlp_modifier is None
and block.requests is None
and not block.skip_exception_verification
and not (
block.expected_block_access_list is not None
and block.expected_block_access_list.modifier is not None
)
):
# Only verify block level exception if:
# - No transaction exception was raised, because these are not reported as block
# 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 transaction exception was raised, because these are not
# reported as block 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.
built_block.verify_block_exception(
transition_tool_exceptions_reliable=t8n.exception_mapper.reliable,
)
Expand Down Expand Up @@ -683,6 +733,9 @@ def make_fixture(
last_block=i == len(self.blocks) - 1,
)
fixture_blocks.append(built_block.get_fixture_block())

# BAL verification already done in to_fixture_bal() if expected_block_access_list set

if block.exception is None:
# Update env, alloc and last block hash for the next block.
alloc = built_block.alloc
Expand Down
Loading
Loading