Skip to content

Commit 85c6fff

Browse files
marioevzdanceratopzspencer-tb
authored
fix(testing): Optimize fill (#1804)
* fix(t8n): introduce profiler * profiler pauses * fix(testing/base_types): Speed up to_bytes fix(base_types): unnecessary cast * fix(testing/base_types): CoerceBytes * fix(testing): Optimize pre-alloc grouping state hash * feat(t8n): introduce LazyAlloc * feat(fixtures): Cached genesis model dump * refactor: prefer explicit cache over circular reference (#2) * Update packages/testing/src/execution_testing/client_clis/cli_types.py Co-authored-by: spencer <[email protected]> * fix: verify modified gas limit * refactor: Use generics * refactor: Add state root cache * docs: changelog * feat(client_clis): Add unit tests --------- Co-authored-by: danceratopz <[email protected]> Co-authored-by: spencer <[email protected]>
1 parent 5919c5b commit 85c6fff

File tree

20 files changed

+820
-435
lines changed

20 files changed

+820
-435
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Test fixtures for use by clients are available for each release on the [Github r
1818

1919
- 🐞 Allow `evmone` to fill Prague and Osaka blockchain tests (mainly modified deposit contract tests) ([#1689](https://github.com/ethereum/execution-specs/pull/1689)).
2020
- 🐞 Turn off Block-Level Access List related checks when filling tests for Amsterdam ([#1737](https://github.com/ethereum/execution-specs/pull/1737)).
21+
- ✨ Optimize the filling process by lazily loading the t8n response only when it’s actually needed. Otherwise, pass it verbatim into the next t8n execution, skipping both pydantic validation and model serialization entirely ([#1804](https://github.com/ethereum/execution-specs/pull/1804)).
2122

2223
#### `consume`
2324

packages/testing/src/execution_testing/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Account,
66
Address,
77
Bytes,
8+
CoerceBytes,
89
Hash,
910
Storage,
1011
TestAddress,
@@ -199,6 +200,7 @@
199200
"TransactionType",
200201
"UndefinedOpcodes",
201202
"While",
203+
"CoerceBytes",
202204
"Withdrawal",
203205
"WithdrawalRequest",
204206
"add_kzg_version",

packages/testing/src/execution_testing/base_types/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
BLSPublicKey,
77
BLSSignature,
88
Bytes,
9+
CoerceBytes,
910
FixedSizeBytes,
1011
ForkHash,
1112
Hash,
@@ -56,6 +57,7 @@
5657
"BLSSignature",
5758
"Bytes",
5859
"CamelModel",
60+
"CoerceBytes",
5961
"EmptyOmmersRoot",
6062
"EmptyTrieRoot",
6163
"EthereumTestBaseModel",

packages/testing/src/execution_testing/base_types/base_types.py

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Basic type primitives used to define other types."""
22

33
from hashlib import sha256
4+
from re import sub
45
from typing import Annotated, Any, ClassVar, SupportsBytes, Type, TypeVar
56

67
from Crypto.Hash import keccak
@@ -57,15 +58,6 @@ def hex(self) -> str:
5758
"""Return the hexadecimal representation of the number."""
5859
return hex(self)
5960

60-
@classmethod
61-
def or_none(
62-
cls: Type[Self], input_number: Self | NumberConvertible | None
63-
) -> Self | None:
64-
"""Convert the input to a Number while accepting None."""
65-
if input_number is None:
66-
return input_number
67-
return cls(input_number)
68-
6961

7062
class Wei(Number):
7163
"""Class that helps represent wei that can be parsed from strings."""
@@ -198,15 +190,6 @@ def hex(self, *args: Any, **kwargs: Any) -> str:
198190
"""Return the hexadecimal representation of the bytes."""
199191
return "0x" + super().hex(*args, **kwargs)
200192

201-
@classmethod
202-
def or_none(
203-
cls, input_bytes: "Bytes | BytesConvertible | None"
204-
) -> "Bytes | None":
205-
"""Convert the input to a Bytes while accepting None."""
206-
if input_bytes is None:
207-
return input_bytes
208-
return cls(input_bytes)
209-
210193
def keccak256(self) -> "Hash":
211194
"""Return the keccak256 hash of the opcode byte representation."""
212195
k = keccak.new(digest_bits=256)
@@ -235,6 +218,21 @@ def __get_pydantic_core_schema__(
235218
)
236219

237220

221+
class CoerceBytes(Bytes):
222+
"""
223+
Class that helps represent bytes of variable length in tests and
224+
supports removing white spaces anywhere in the string.
225+
"""
226+
227+
def __new__(cls, input_bytes: BytesConvertible = b"") -> Self:
228+
"""Create a new CoerceBytes object."""
229+
if type(input_bytes) is cls:
230+
return input_bytes
231+
if isinstance(input_bytes, str):
232+
input_bytes = sub(r"\s+", "", input_bytes)
233+
return super(Bytes, cls).__new__(cls, to_bytes(input_bytes))
234+
235+
238236
class FixedSizeHexNumber(int, ToStringSchema):
239237
"""
240238
A base class that helps represent an integer as a fixed byte-length
@@ -347,15 +345,6 @@ def __hash__(self) -> int:
347345
"""Return the hash of the bytes."""
348346
return super(FixedSizeBytes, self).__hash__()
349347

350-
@classmethod
351-
def or_none(
352-
cls: Type[Self], input_bytes: Self | FixedSizeBytesConvertible | None
353-
) -> Self | None:
354-
"""Convert the input to a Fixed Size Bytes while accepting None."""
355-
if input_bytes is None:
356-
return input_bytes
357-
return cls(input_bytes)
358-
359348
def __eq__(self, other: object) -> bool:
360349
"""Compare two FixedSizeBytes objects to be equal."""
361350
if other is None:

packages/testing/src/execution_testing/base_types/conversions.py

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Common conversion methods."""
22

3-
from re import sub
4-
from typing import Any, List, Optional, SupportsBytes, TypeAlias
3+
from typing import List, SupportsBytes, TypeAlias
54

65
BytesConvertible: TypeAlias = str | bytes | SupportsBytes | List[int]
76
FixedSizeBytesConvertible: TypeAlias = (
@@ -10,46 +9,20 @@
109
NumberConvertible: TypeAlias = str | bytes | SupportsBytes | int
1110

1211

13-
def int_or_none(input_value: Any, default: Optional[int] = None) -> int | None:
14-
"""Convert a value to int or returns a default (None)."""
15-
if input_value is None:
16-
return default
17-
if isinstance(input_value, int):
18-
return input_value
19-
return int(input_value, 0)
20-
21-
22-
def str_or_none(input_value: Any, default: Optional[str] = None) -> str | None:
23-
"""Convert a value to string or returns a default (None)."""
24-
if input_value is None:
25-
return default
26-
if isinstance(input_value, str):
27-
return input_value
28-
return str(input_value)
29-
30-
3112
def to_bytes(input_bytes: BytesConvertible) -> bytes:
3213
"""Convert multiple types into bytes."""
3314
if input_bytes is None:
3415
raise Exception("Cannot convert `None` input to bytes")
3516

36-
if (
37-
isinstance(input_bytes, SupportsBytes)
38-
or isinstance(input_bytes, bytes)
39-
or isinstance(input_bytes, list)
40-
):
41-
return bytes(input_bytes)
42-
4317
if isinstance(input_bytes, str):
4418
# We can have a hex representation of bytes with spaces for readability
45-
input_bytes = sub(r"\s+", "", input_bytes)
4619
if input_bytes.startswith("0x"):
4720
input_bytes = input_bytes[2:]
4821
if len(input_bytes) % 2 == 1:
4922
input_bytes = "0" + input_bytes
5023
return bytes.fromhex(input_bytes)
5124

52-
raise Exception("invalid type for `bytes`")
25+
return bytes(input_bytes)
5326

5427

5528
def to_fixed_size_bytes(
@@ -85,9 +58,9 @@ def to_fixed_size_bytes(
8558
)
8659
if len(input_bytes) < size:
8760
if left_padding:
88-
return bytes(input_bytes).rjust(size, b"\x00")
61+
return input_bytes.rjust(size, b"\x00")
8962
if right_padding:
90-
return bytes(input_bytes).ljust(size, b"\x00")
63+
return input_bytes.ljust(size, b"\x00")
9164
raise Exception(
9265
f"input is too small for fixed size bytes: {len(input_bytes)} < {size}\n"
9366
"Use `left_padding=True` or `right_padding=True` to allow padding."

packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py

Lines changed: 14 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
FixtureFillingPhase,
4141
LabeledFixtureFormat,
4242
PreAllocGroup,
43+
PreAllocGroupBuilder,
44+
PreAllocGroupBuilders,
4345
PreAllocGroups,
4446
TestInfo,
4547
)
@@ -231,7 +233,8 @@ class FillingSession:
231233
fixture_output: FixtureOutput
232234
phase_manager: PhaseManager
233235
format_selector: FormatSelector
234-
pre_alloc_groups: PreAllocGroups | None
236+
pre_alloc_groups: PreAllocGroups | None = None
237+
pre_alloc_group_builders: PreAllocGroupBuilders | None = None
235238

236239
@classmethod
237240
def from_config(cls, config: pytest.Config) -> "Self":
@@ -263,7 +266,7 @@ def _initialize_pre_alloc_groups(self) -> None:
263266
"""Initialize pre-allocation groups based on the current phase."""
264267
if self.phase_manager.is_pre_alloc_generation:
265268
# Phase 1: Create empty container for collecting groups
266-
self.pre_alloc_groups = PreAllocGroups(root={})
269+
self.pre_alloc_group_builders = PreAllocGroupBuilders(root={})
267270
elif self.phase_manager.is_fill_after_pre_alloc:
268271
# Phase 2: Load pre-alloc groups from disk
269272
self._load_pre_alloc_groups_from_folder()
@@ -326,15 +329,15 @@ def get_pre_alloc_group(self, hash_key: str) -> PreAllocGroup:
326329

327330
return self.pre_alloc_groups[hash_key]
328331

329-
def update_pre_alloc_group(
330-
self, hash_key: str, group: PreAllocGroup
332+
def update_pre_alloc_group_builder(
333+
self, hash_key: str, group_builder: PreAllocGroupBuilder
331334
) -> None:
332335
"""
333336
Update or add a pre-allocation group.
334337
335338
Args:
336339
hash_key: The hash of the pre-alloc group.
337-
group: The pre-allocation group.
340+
group_builder: The pre-allocation group builder.
338341
339342
Raises:
340343
ValueError: If not in pre-alloc generation phase.
@@ -345,44 +348,19 @@ def update_pre_alloc_group(
345348
"Can only update pre-alloc groups in generation phase"
346349
)
347350

348-
if self.pre_alloc_groups is None:
349-
self.pre_alloc_groups = PreAllocGroups(root={})
351+
if self.pre_alloc_group_builders is None:
352+
self.pre_alloc_group_builders = PreAllocGroupBuilders(root={})
350353

351-
self.pre_alloc_groups[hash_key] = group
354+
self.pre_alloc_group_builders.root[hash_key] = group_builder
352355

353356
def save_pre_alloc_groups(self) -> None:
354357
"""Save pre-allocation groups to disk."""
355-
if self.pre_alloc_groups is None:
358+
if self.pre_alloc_group_builders is None:
356359
return
357360

358361
pre_alloc_folder = self.fixture_output.pre_alloc_groups_folder_path
359362
pre_alloc_folder.mkdir(parents=True, exist_ok=True)
360-
self.pre_alloc_groups.to_folder(pre_alloc_folder)
361-
362-
def aggregate_pre_alloc_groups(
363-
self, worker_groups: PreAllocGroups
364-
) -> None:
365-
"""
366-
Aggregate pre-alloc groups from a worker process (xdist support).
367-
368-
Args:
369-
worker_groups: Pre-alloc groups from a worker process.
370-
371-
"""
372-
if self.pre_alloc_groups is None:
373-
self.pre_alloc_groups = PreAllocGroups(root={})
374-
375-
for hash_key, group in worker_groups.items():
376-
if hash_key in self.pre_alloc_groups:
377-
# Merge if exists (should not happen in practice)
378-
existing = self.pre_alloc_groups[hash_key]
379-
if existing.pre != group.pre:
380-
raise ValueError(
381-
f"Conflicting pre-alloc groups for hash {hash_key}: "
382-
f"existing={self.pre_alloc_groups[hash_key].pre}, new={group.pre}"
383-
)
384-
else:
385-
self.pre_alloc_groups[hash_key] = group
363+
self.pre_alloc_group_builders.to_folder(pre_alloc_folder)
386364

387365

388366
def calculate_post_state_diff(
@@ -1382,7 +1360,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
13821360
# Use the original update_pre_alloc_groups method which
13831361
# returns the groups
13841362
self.update_pre_alloc_groups(
1385-
session.pre_alloc_groups, request.node.nodeid
1363+
session.pre_alloc_group_builders, request.node.nodeid
13861364
)
13871365
return # Skip fixture generation in phase 1
13881366

0 commit comments

Comments
 (0)