Skip to content

Commit 5a87a65

Browse files
committed
feat(static): implement address tag resolution for static test fillers
Hard-coded address conversion in yml and json fillers: - Add convert_addresses.py script to automate tag conversion - The correct way to run this is with the ``CONVERT_COINBASE`` flag set to ``False`` as this allows the same coinbase for all tests (just as python tests do). If we decide we want to handle the coinbase setting on the python side, we can turn this flag on and hard-code on the python side... but the currect approach seems correct. - Convert 1000+ static test YAML/JSON files to use address tags (Python) Generate deterministic addresses from tags coming from static test fillers: - Resolve tags to deterministic addresses in the same way python tests do - via pytest static filler plugin - Add ``BlockchainEngineXFixture`` support for pre-allocation groups This enables static tests to use symbolic address tags instead of hardcoded addresses, minimizing muddied context across tests when running via pre alloc sharing.
1 parent 73e2d89 commit 5a87a65

File tree

2,481 files changed

+26457
-25498
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

2,481 files changed

+26457
-25498
lines changed

convert_addresses.py

Lines changed: 475 additions & 0 deletions
Large diffs are not rendered by default.

src/ethereum_test_specs/static_state/account.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from pydantic import BaseModel
66

7-
from .common import CodeInFiller, ValueInFiller
7+
from .common import CodeInFiller, ValueInFiller, ValueOrTagInFiller
88

99

1010
class AccountInFiller(BaseModel):
@@ -13,7 +13,7 @@ class AccountInFiller(BaseModel):
1313
balance: ValueInFiller
1414
code: CodeInFiller
1515
nonce: ValueInFiller
16-
storage: Dict[ValueInFiller, ValueInFiller]
16+
storage: Dict[ValueInFiller, ValueOrTagInFiller]
1717

1818
class Config:
1919
"""Model Config."""
Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
"""Ethereum/tests structures."""
22

3-
from .common import AddressInFiller, CodeInFiller, Hash32InFiller, ValueInFiller
3+
from .common import (
4+
AccessListInFiller,
5+
AddressInFiller,
6+
AddressOrTagInFiller,
7+
AddressTag,
8+
CodeInFiller,
9+
Hash32InFiller,
10+
Hash32OrTagInFiller,
11+
ValueInFiller,
12+
ValueOrTagInFiller,
13+
parse_address_or_tag,
14+
)
415

5-
__all__ = ["AddressInFiller", "ValueInFiller", "CodeInFiller", "Hash32InFiller"]
16+
__all__ = [
17+
"AccessListInFiller",
18+
"AddressInFiller",
19+
"AddressOrTagInFiller",
20+
"AddressTag",
21+
"ValueInFiller",
22+
"ValueOrTagInFiller",
23+
"CodeInFiller",
24+
"Hash32InFiller",
25+
"Hash32OrTagInFiller",
26+
"parse_address_or_tag",
27+
]

src/ethereum_test_specs/static_state/common/common.py

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import subprocess
55
import tempfile
66
from functools import cached_property
7-
from typing import Any
7+
from typing import Any, List, Union
88

99
from eth_abi import encode
1010
from eth_utils import function_signature_to_4byte_selector
11+
from pydantic import BaseModel, Field, field_validator
1112
from pydantic.functional_validators import BeforeValidator
13+
from pydantic_core import core_schema
1214
from typing_extensions import Annotated
1315

1416
from ethereum_test_base_types import Address, Hash, HexNumber
@@ -30,6 +32,22 @@ def parse_hex_number(i: str | int) -> int:
3032
return int(i, 10)
3133

3234

35+
def parse_value_or_address_tag(value: Any) -> Union[HexNumber, str]:
36+
"""Parse either a hex number or an address tag for storage values."""
37+
if not isinstance(value, str):
38+
# Non-string values should be converted to HexNumber normally
39+
return HexNumber(parse_hex_number(value))
40+
41+
# Check if it matches address tag pattern: <type:name:0xaddress> or <type:0xaddress>
42+
tag_pattern = r"^<(eoa|contract):.+>$"
43+
if re.match(tag_pattern, value.strip()):
44+
# Return the tag string as-is for later resolution
45+
return value.strip()
46+
else:
47+
# Parse as hex number
48+
return HexNumber(parse_hex_number(value))
49+
50+
3351
def parse_args_from_string_into_array(stream: str, pos: int, delim: str = " "):
3452
"""Parse YUL options into array."""
3553
args = []
@@ -186,7 +204,121 @@ def parse_code_label(code) -> CodeInFillerSource:
186204
return CodeInFillerSource(code, label)
187205

188206

207+
class AddressTag:
208+
"""Represents an address tag like <eoa:sender> or <contract:token>."""
209+
210+
def __init__(self, tag_type: str, tag_name: str, original_string: str):
211+
"""Initialize address tag."""
212+
self.tag_type = tag_type # "eoa" or "contract"
213+
self.tag_name = tag_name # e.g., "sender", "token"
214+
self.original_string = original_string
215+
216+
def __str__(self) -> str:
217+
"""Return original tag string."""
218+
return self.original_string
219+
220+
def __repr__(self) -> str:
221+
"""Return debug representation."""
222+
return f"AddressTag(type={self.tag_type}, name={self.tag_name})"
223+
224+
def __eq__(self, other) -> bool:
225+
"""Check equality based on original string."""
226+
if isinstance(other, AddressTag):
227+
return self.original_string == other.original_string
228+
return False
229+
230+
def __hash__(self) -> int:
231+
"""Hash based on original string for use as dict key."""
232+
return hash(self.original_string)
233+
234+
@classmethod
235+
def __get_pydantic_core_schema__(cls, source_type: Any, handler) -> core_schema.CoreSchema:
236+
"""Pydantic core schema for AddressTag."""
237+
return core_schema.str_schema()
238+
239+
240+
def parse_address_or_tag(value: Any) -> Union[Address, AddressTag]:
241+
"""Parse either a regular address or an address tag."""
242+
if not isinstance(value, str):
243+
# Non-string values should be converted to Address normally
244+
return Address(value, left_padding=True)
245+
246+
# Check if it matches tag pattern:
247+
# - <eoa:0x...> or <contract:0x...>
248+
# - <eoa:name:0x...> or <contract:name:0x...>
249+
tag_pattern = r"^<(eoa|contract):(.+)>$"
250+
match = re.match(tag_pattern, value.strip())
251+
252+
if match:
253+
tag_type = match.group(1)
254+
# The tag_name is everything after the type and colon
255+
# Could be "0x1234..." or "sender:0x1234..."
256+
tag_name = match.group(2)
257+
258+
return AddressTag(tag_type, tag_name, value.strip())
259+
else:
260+
# Regular address string
261+
return Address(value, left_padding=True)
262+
263+
264+
def parse_address_or_tag_for_access_list(value: Any) -> Union[Address, str]:
265+
"""
266+
Parse either a regular address or an address tag, keeping tags as strings for later
267+
resolution.
268+
"""
269+
if not isinstance(value, str):
270+
# Non-string values should be converted to Address normally
271+
return Address(value, left_padding=True)
272+
273+
# Check if it matches a tag pattern
274+
tag_pattern = r"^<(eoa|contract):.+>$"
275+
if re.match(tag_pattern, value.strip()):
276+
# Return the tag string as-is for later resolution
277+
return value.strip()
278+
else:
279+
# Regular address string
280+
return Address(value, left_padding=True)
281+
282+
283+
def validate_address_or_tag_string(value: Union[Address, str]) -> Union[Address, str]:
284+
"""Validate and normalize address or tag as string for later resolution."""
285+
if isinstance(value, str):
286+
return value.strip()
287+
else:
288+
return Address(value, left_padding=True)
289+
290+
291+
def parse_hash32_or_sender_key_tag(value: Any) -> Union[Hash, str]:
292+
"""Parse either a regular hash or a sender key tag for later resolution."""
293+
if isinstance(value, str) and value.strip().startswith("<sender:key:"):
294+
return value.strip()
295+
return Hash(value, left_padding=True)
296+
297+
189298
AddressInFiller = Annotated[Address, BeforeValidator(lambda a: Address(a, left_padding=True))]
299+
AddressOrTagInFiller = Annotated[
300+
Union[Address, str], BeforeValidator(validate_address_or_tag_string)
301+
]
302+
ValueOrTagInFiller = Annotated[Union[HexNumber, str], BeforeValidator(parse_value_or_address_tag)]
303+
Hash32OrTagInFiller = Annotated[Union[Hash, str], BeforeValidator(parse_hash32_or_sender_key_tag)]
190304
ValueInFiller = Annotated[HexNumber, BeforeValidator(parse_hex_number)]
191305
CodeInFiller = Annotated[CodeInFillerSource, BeforeValidator(parse_code_label)]
192306
Hash32InFiller = Annotated[Hash, BeforeValidator(lambda h: Hash(h, left_padding=True))]
307+
308+
309+
class AccessListInFiller(BaseModel):
310+
"""Access List for transactions in fillers that can contain address tags."""
311+
312+
address: Union[Address, str] # Can be an address or a tag string
313+
storage_keys: List[Hash] = Field([], alias="storageKeys")
314+
315+
@field_validator("address", mode="before")
316+
@classmethod
317+
def validate_address(cls, v):
318+
"""Allow both addresses and tags."""
319+
return parse_address_or_tag_for_access_list(v)
320+
321+
class Config:
322+
"""Model config."""
323+
324+
populate_by_name = True

src/ethereum_test_specs/static_state/expect_section.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ethereum_test_exceptions import TransactionExceptionInstanceOrList
1010
from ethereum_test_forks import get_forks
1111

12-
from .common import AddressInFiller, CodeInFiller, ValueInFiller
12+
from .common import AddressOrTagInFiller, CodeInFiller, ValueInFiller, ValueOrTagInFiller
1313

1414

1515
class Indexes(BaseModel):
@@ -31,7 +31,7 @@ class AccountInExpectSection(BaseModel):
3131
balance: ValueInFiller | None = Field(None)
3232
code: CodeInFiller | None = Field(None)
3333
nonce: ValueInFiller | None = Field(None)
34-
storage: Dict[ValueInFiller, ValueInFiller | Literal["ANY"]] | None = Field(None)
34+
storage: Dict[ValueOrTagInFiller, ValueOrTagInFiller | Literal["ANY"]] | None = Field(None)
3535
expected_to_not_exist: str | int | None = Field(None, alias="shouldnotexist")
3636

3737
class Config:
@@ -108,7 +108,7 @@ class ExpectSectionInStateTestFiller(CamelModel):
108108

109109
indexes: Indexes = Field(default_factory=Indexes)
110110
network: List[str]
111-
result: Dict[AddressInFiller, AccountInExpectSection]
111+
result: Dict[AddressOrTagInFiller, AccountInExpectSection]
112112
expect_exception: Dict[str, TransactionExceptionInstanceOrList] | None = None
113113

114114
class Config:

src/ethereum_test_specs/static_state/general_transaction.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,23 @@
44

55
from pydantic import BaseModel, Field, field_validator, model_validator
66

7-
from ethereum_test_base_types import AccessList, Hash
7+
from ethereum_test_base_types import Hash
88

9-
from .common import AddressInFiller, CodeInFiller, Hash32InFiller, ValueInFiller
9+
from .common import (
10+
AccessListInFiller,
11+
AddressOrTagInFiller,
12+
CodeInFiller,
13+
Hash32InFiller,
14+
Hash32OrTagInFiller,
15+
ValueInFiller,
16+
)
1017

1118

1219
class DataWithAccessList(BaseModel):
1320
"""Class that represents data with access list."""
1421

1522
data: CodeInFiller
16-
access_list: List[AccessList] | None = Field(None, alias="accessList")
23+
access_list: List[AccessListInFiller] | None = Field(None, alias="accessList")
1724

1825
class Config:
1926
"""Model Config."""
@@ -47,9 +54,9 @@ class GeneralTransactionInFiller(BaseModel):
4754
gas_limit: List[ValueInFiller] = Field(..., alias="gasLimit")
4855
gas_price: ValueInFiller | None = Field(None, alias="gasPrice")
4956
nonce: ValueInFiller
50-
to: AddressInFiller | None
57+
to: AddressOrTagInFiller | None
5158
value: List[ValueInFiller]
52-
secret_key: Hash32InFiller = Field(..., alias="secretKey")
59+
secret_key: Hash32OrTagInFiller = Field(..., alias="secretKey")
5360

5461
max_fee_per_gas: ValueInFiller | None = Field(None, alias="maxFeePerGas")
5562
max_priority_fee_per_gas: ValueInFiller | None = Field(None, alias="maxPriorityFeePerGas")

0 commit comments

Comments
 (0)