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
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
in a block access list as defined in EIP-7928.
"""

from typing import ClassVar, List, Union
from typing import ClassVar, List, Self, Union

from pydantic import Field
from pydantic import Field, model_validator

from execution_testing.base_types import (
Address,
Expand Down Expand Up @@ -79,9 +79,28 @@ class BalStorageSlot(CamelModel, RLPSerializable):
slot_changes: List[BalStorageChange] = Field(
default_factory=list, description="List of changes to this slot"
)
validate_any_change: bool = Field(
default=False,
description=(
"If True, asserts at least one change exists in this slot "
"without validating specific values. Mutually exclusive with "
"non-empty slot_changes."
),
exclude=True,
)

rlp_fields: ClassVar[List[str]] = ["slot", "slot_changes"]

@model_validator(mode="after")
def _check_mutual_exclusion(self) -> Self:
if self.validate_any_change and self.slot_changes:
raise ValueError(
"Cannot set both validate_any_change=True and slot_changes. "
"Use validate_any_change=True to assert at least one change "
"exists, or slot_changes=[...] to validate specific changes."
)
return self


class BalAccountChange(CamelModel, RLPSerializable):
"""Represents all changes to a specific account in a block."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,23 @@ def _compare_account_expectations(
].slot_changes
expected_slot_changes = expected_slot.slot_changes

if not expected_slot_changes:
# Empty expected means any
# slot_changes are acceptable
pass
if expected_slot.validate_any_change:
# Assert at least one change exists
if not actual_slot_changes:
raise BlockAccessListValidationError(
f"Expected at least one change "
f"in slot {expected_slot.slot} "
f"(validate_any_change=True) "
f"but found none"
)
elif not expected_slot_changes:
# Explicitly empty = assert no changes
if actual_slot_changes:
raise BlockAccessListValidationError(
f"Expected no changes in slot "
f"{expected_slot.slot} but found "
f"{actual_slot_changes}"
)
else:
# Validate slot_changes as subsequence
slot_actual_idx = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1102,3 +1102,157 @@ def test_bal_account_absent_values_empty_slot_changes_raises() -> None:
)
]
)


# --- Tests for BalStorageSlot.validate_any_change ---


def test_validate_any_change_passes_with_non_empty_actual() -> None:
"""validate_any_change=True passes when actual has at least one change."""
addr = Address(0xA)

actual_bal = BlockAccessList(
[
BalAccountChange(
address=addr,
storage_changes=[
BalStorageSlot(
slot=0x01,
slot_changes=[
BalStorageChange(
block_access_index=0, post_value=0xBEEF
)
],
),
],
)
]
)

expectation = BlockAccessListExpectation(
account_expectations={
addr: BalAccountExpectation(
storage_changes=[
BalStorageSlot(slot=0x01, validate_any_change=True),
],
),
}
)

expectation.verify_against(actual_bal)


def test_validate_any_change_fails_with_empty_actual() -> None:
"""validate_any_change=True fails when actual has no changes."""
addr = Address(0xA)

actual_bal = BlockAccessList(
[
BalAccountChange(
address=addr,
storage_changes=[
BalStorageSlot(slot=0x01, slot_changes=[]),
],
)
]
)

expectation = BlockAccessListExpectation(
account_expectations={
addr: BalAccountExpectation(
storage_changes=[
BalStorageSlot(slot=0x01, validate_any_change=True),
],
),
}
)

with pytest.raises(
BlockAccessListValidationError,
match="Expected at least one change in slot",
):
expectation.verify_against(actual_bal)


def test_validate_any_change_mutual_exclusion_with_slot_changes() -> None:
"""
validate_any_change=True and non-empty slot_changes raises ValueError.
"""
with pytest.raises(
ValueError,
match="Cannot set both validate_any_change=True and slot_changes",
):
BalStorageSlot(
slot=0x01,
validate_any_change=True,
slot_changes=[
BalStorageChange(block_access_index=0, post_value=0xBEEF)
],
)


def test_slot_changes_empty_asserts_no_changes() -> None:
"""slot_changes=[] asserts that actual has no changes."""
addr = Address(0xA)

actual_bal = BlockAccessList(
[
BalAccountChange(
address=addr,
storage_changes=[
BalStorageSlot(
slot=0x01,
slot_changes=[
BalStorageChange(
block_access_index=0, post_value=0xBEEF
)
],
),
],
)
]
)

expectation = BlockAccessListExpectation(
account_expectations={
addr: BalAccountExpectation(
storage_changes=[
BalStorageSlot(slot=0x01, slot_changes=[]),
],
),
}
)

with pytest.raises(
BlockAccessListValidationError,
match="Expected no changes in slot",
):
expectation.verify_against(actual_bal)


def test_slot_changes_empty_passes_when_actual_empty() -> None:
"""slot_changes=[] passes when actual also has no changes."""
addr = Address(0xA)

actual_bal = BlockAccessList(
[
BalAccountChange(
address=addr,
storage_changes=[
BalStorageSlot(slot=0x01, slot_changes=[]),
],
)
]
)

expectation = BlockAccessListExpectation(
account_expectations={
addr: BalAccountExpectation(
storage_changes=[
BalStorageSlot(slot=0x01, slot_changes=[]),
],
),
}
)

expectation.verify_against(actual_bal)
Loading
Loading