Skip to content
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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ Expand cases to test *CALL opcodes causing OOG ([#1703](https://github.com/ethereum/execution-specs/pull/1703)).
- ✨ Add tests for `modexp` and `ripemd` precompiled contracts ([#1691](https://github.com/ethereum/execution-specs/pull/1691)).
- ✨ Add `ecrecover` precompile tests originating form `evmone` unittests ([#1685](https://github.com/ethereum/execution-specs/pull/1685)).
- 🐞 Fix `BlockAccessList` validation for T8N to run independent of whether a `BlockAccessListExpectation` is defined for the test ([#1742](https://github.com/ethereum/execution-specs/pull/1742)).

## [v5.3.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v5.3.0) - 2025-10-09

Expand Down
6 changes: 6 additions & 0 deletions packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,12 @@ def generate_block_data(
# tests
t8n_bal = transition_tool_output.result.block_access_list
bal = t8n_bal

# Always validate BAL structural integrity (ordering, duplicates) if present
if t8n_bal is not None:
t8n_bal.validate_structure()
Comment on lines +719 to +721
Copy link
Contributor

Choose a reason for hiding this comment

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

As far as I can tell this is the only new code and the rest is refactored?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep!


# If expected BAL is defined, verify against it
if (
block.expected_block_access_list is not None
and t8n_bal is not None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
BalCodeChange,
BalNonceChange,
BalStorageSlot,
BlockAccessListChangeLists,
)
from .exceptions import BlockAccessListValidationError
from .t8n import BlockAccessList
Expand Down Expand Up @@ -179,9 +178,8 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None:
Verify that the actual BAL from the client matches this expected BAL.

Validation steps:
1. Validate actual BAL conforms to EIP-7928 ordering requirements
2. Verify address expectations - presence or explicit absence
3. Verify expected changes within accounts match actual changes
1. Verify address expectations - presence or explicit absence
2. Verify expected changes within accounts match actual changes

Args:
actual_bal: The BlockAccessList model from the client
Expand All @@ -190,9 +188,6 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None:
BlockAccessListValidationError: If verification fails

"""
# validate the actual BAL structure follows EIP-7928 ordering
self._validate_bal_ordering(actual_bal)

actual_accounts_by_addr = {acc.address: acc for acc in actual_bal.root}
for address, expectation in self.account_expectations.items():
if expectation is None:
Expand Down Expand Up @@ -236,111 +231,6 @@ def verify_against(self, actual_bal: "BlockAccessList") -> None:
f"Account {address}: {str(e)}"
) from e

@staticmethod
def _validate_bal_ordering(bal: "BlockAccessList") -> None:
"""
Validate BAL ordering follows EIP-7928 requirements.

Args:
bal: The BlockAccessList to validate

Raises:
BlockAccessListValidationError: If ordering is invalid

"""
# Check address ordering (ascending)
for i in range(1, len(bal.root)):
if bal.root[i - 1].address >= bal.root[i].address:
raise BlockAccessListValidationError(
f"BAL addresses are not in lexicographic order: "
f"{bal.root[i - 1].address} >= {bal.root[i].address}"
)

# Check transaction index ordering and uniqueness within accounts
for account in bal.root:
changes_to_check: List[tuple[str, BlockAccessListChangeLists]] = [
("nonce_changes", account.nonce_changes),
("balance_changes", account.balance_changes),
("code_changes", account.code_changes),
]

for field_name, change_list in changes_to_check:
if not change_list:
continue

tx_indices = [c.tx_index for c in change_list]

# Check both ordering and duplicates
if tx_indices != sorted(tx_indices):
raise BlockAccessListValidationError(
f"Transaction indices not in ascending order in {field_name} of account "
f"{account.address}. Got: {tx_indices}, Expected: {sorted(tx_indices)}"
)

if len(tx_indices) != len(set(tx_indices)):
duplicates = sorted(
{
idx
for idx in tx_indices
if tx_indices.count(idx) > 1
}
)
raise BlockAccessListValidationError(
f"Duplicate transaction indices in {field_name} of account "
f"{account.address}. Duplicates: {duplicates}"
)

# Check storage slot ordering
for i in range(1, len(account.storage_changes)):
if (
account.storage_changes[i - 1].slot
>= account.storage_changes[i].slot
):
raise BlockAccessListValidationError(
f"Storage slots not in ascending order in account "
f"{account.address}: {account.storage_changes[i - 1].slot} >= "
f"{account.storage_changes[i].slot}"
)

# Check transaction index ordering and uniqueness within storage
# slots
for storage_slot in account.storage_changes:
if not storage_slot.slot_changes:
continue

tx_indices = [c.tx_index for c in storage_slot.slot_changes]

# Check both ordering and duplicates
if tx_indices != sorted(tx_indices):
raise BlockAccessListValidationError(
f"Transaction indices not in ascending order in storage slot "
f"{storage_slot.slot} of account {account.address}. "
f"Got: {tx_indices}, Expected: {sorted(tx_indices)}"
)

if len(tx_indices) != len(set(tx_indices)):
duplicates = sorted(
{
idx
for idx in tx_indices
if tx_indices.count(idx) > 1
}
)
raise BlockAccessListValidationError(
f"Duplicate transaction indices in storage slot "
f"{storage_slot.slot} of account {account.address}. "
f"Duplicates: {duplicates}"
)

# Check storage reads ordering
for i in range(1, len(account.storage_reads)):
if account.storage_reads[i - 1] >= account.storage_reads[i]:
raise BlockAccessListValidationError(
f"Storage reads not in ascending order in account "
f"{account.address}: {account.storage_reads[i - 1]} >= "
f"{account.storage_reads[i]}"
)

@staticmethod
def _compare_account_expectations(
expected: BalAccountExpectation, actual: BalAccountChange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
)

from .account_changes import BalAccountChange
from .exceptions import BlockAccessListValidationError


class BlockAccessList(EthereumTestRootModel[List[BalAccountChange]]):
Expand Down Expand Up @@ -49,3 +50,108 @@ def rlp(self) -> Bytes:
def rlp_hash(self) -> Bytes:
"""Return the hash of the RLP encoded block access list."""
return self.rlp.keccak256()

def validate_structure(self) -> None:
"""
Validate BAL structure follows EIP-7928 requirements.

Checks:
- Addresses are in lexicographic (ascending) order
- Transaction indices are sorted and unique within each change list
- Storage slots are in ascending order
- Storage reads are in ascending order

Raises:
BlockAccessListValidationError: If validation fails
"""
# Check address ordering (ascending)
for i in range(1, len(self.root)):
if self.root[i - 1].address >= self.root[i].address:
raise BlockAccessListValidationError(
f"BAL addresses are not in lexicographic order: "
f"{self.root[i - 1].address} >= {self.root[i].address}"
)

# Check transaction index ordering and uniqueness within accounts
for account in self.root:
changes_to_check: List[tuple[str, List[Any]]] = [
("nonce_changes", account.nonce_changes),
("balance_changes", account.balance_changes),
("code_changes", account.code_changes),
]

for field_name, change_list in changes_to_check:
if not change_list:
continue

tx_indices = [c.tx_index for c in change_list]

# Check both ordering and duplicates
if tx_indices != sorted(tx_indices):
raise BlockAccessListValidationError(
f"Transaction indices not in ascending order in {field_name} of account "
f"{account.address}. Got: {tx_indices}, Expected: {sorted(tx_indices)}"
)

if len(tx_indices) != len(set(tx_indices)):
duplicates = sorted(
{
idx
for idx in tx_indices
if tx_indices.count(idx) > 1
}
)
raise BlockAccessListValidationError(
f"Duplicate transaction indices in {field_name} of account "
f"{account.address}. Duplicates: {duplicates}"
)

# Check storage slot ordering
for i in range(1, len(account.storage_changes)):
if (
account.storage_changes[i - 1].slot
>= account.storage_changes[i].slot
):
raise BlockAccessListValidationError(
f"Storage slots not in ascending order in account "
f"{account.address}: {account.storage_changes[i - 1].slot} >= "
f"{account.storage_changes[i].slot}"
)

# Check transaction index ordering and uniqueness within storage slots
for storage_slot in account.storage_changes:
if not storage_slot.slot_changes:
continue

tx_indices = [c.tx_index for c in storage_slot.slot_changes]

# Check both ordering and duplicates
if tx_indices != sorted(tx_indices):
raise BlockAccessListValidationError(
f"Transaction indices not in ascending order in storage slot "
f"{storage_slot.slot} of account {account.address}. "
f"Got: {tx_indices}, Expected: {sorted(tx_indices)}"
)

if len(tx_indices) != len(set(tx_indices)):
duplicates = sorted(
{
idx
for idx in tx_indices
if tx_indices.count(idx) > 1
}
)
raise BlockAccessListValidationError(
f"Duplicate transaction indices in storage slot "
f"{storage_slot.slot} of account {account.address}. "
f"Duplicates: {duplicates}"
)

# Check storage reads ordering
for i in range(1, len(account.storage_reads)):
if account.storage_reads[i - 1] >= account.storage_reads[i]:
raise BlockAccessListValidationError(
f"Storage reads not in ascending order in account "
f"{account.address}: {account.storage_reads[i - 1]} >= "
f"{account.storage_reads[i]}"
)
Loading
Loading