Skip to content

Commit 808a89b

Browse files
feat: wrap blockchain test for benchmark
1 parent 2f97840 commit 808a89b

File tree

4 files changed

+178
-3
lines changed

4 files changed

+178
-3
lines changed

src/ethereum_test_specs/__init__.py

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

33
from .base import BaseTest, TestSpec
44
from .base_static import BaseStaticTest
5+
from .benchmark import BenchmarkTest, BenchmarkTestFiller, BenchmarkTestSpec
56
from .blobs import BlobsTest, BlobsTestFiller, BlobsTestSpec
67
from .blockchain import (
78
BlockchainTest,
@@ -23,6 +24,9 @@
2324
__all__ = (
2425
"BaseStaticTest",
2526
"BaseTest",
27+
"BenchmarkTest",
28+
"BenchmarkTestFiller",
29+
"BenchmarkTestSpec",
2630
"BlobsTest",
2731
"BlobsTestFiller",
2832
"BlobsTestSpec",

src/ethereum_test_specs/benchmark.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
"""Ethereum benchmark test spec definition and filler."""
2+
3+
from typing import Callable, ClassVar, Dict, Generator, List, Optional, Sequence, Type
4+
5+
import pytest
6+
from pydantic import Field
7+
8+
from ethereum_clis import TransitionTool
9+
from ethereum_test_base_types import HexNumber
10+
from ethereum_test_exceptions import BlockException, TransactionException
11+
from ethereum_test_execution import (
12+
BaseExecute,
13+
ExecuteFormat,
14+
LabeledExecuteFormat,
15+
TransactionPost,
16+
)
17+
from ethereum_test_fixtures import (
18+
BaseFixture,
19+
BlockchainEngineFixture,
20+
BlockchainEngineXFixture,
21+
BlockchainFixture,
22+
FixtureFormat,
23+
LabeledFixtureFormat,
24+
)
25+
from ethereum_test_forks import Fork
26+
from ethereum_test_types import Alloc, Environment, Transaction
27+
28+
from .base import BaseTest
29+
from .blockchain import Block, BlockchainTest
30+
31+
32+
class BenchmarkTest(BaseTest):
33+
"""Test type designed specifically for benchmark test cases."""
34+
35+
pre: Alloc
36+
post: Alloc
37+
tx: Optional[Transaction] = None
38+
blocks: Optional[List[Block]] = None
39+
block_exception: (
40+
List[TransactionException | BlockException] | TransactionException | BlockException | None
41+
) = None
42+
env: Environment = Field(default_factory=Environment)
43+
expected_benchmark_gas_used: int | None = None
44+
45+
supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [
46+
BlockchainFixture,
47+
BlockchainEngineFixture,
48+
BlockchainEngineXFixture,
49+
]
50+
51+
supported_execute_formats: ClassVar[Sequence[LabeledExecuteFormat]] = [
52+
LabeledExecuteFormat(
53+
TransactionPost,
54+
"benchmark_test",
55+
"An execute test derived from a benchmark test",
56+
),
57+
]
58+
59+
supported_markers: ClassVar[Dict[str, str]] = {
60+
"blockchain_test_engine_only": "Only generate a blockchain test engine fixture",
61+
"blockchain_test_only": "Only generate a blockchain test fixture",
62+
}
63+
64+
@classmethod
65+
def pytest_parameter_name(cls) -> str:
66+
"""Return the parameter name used in pytest to select this spec type."""
67+
return "benchmark_test"
68+
69+
@classmethod
70+
def discard_fixture_format_by_marks(
71+
cls,
72+
fixture_format: FixtureFormat,
73+
fork: Fork,
74+
markers: List[pytest.Mark],
75+
) -> bool:
76+
"""Discard a fixture format from filling if the appropriate marker is used."""
77+
if "blockchain_test_only" in [m.name for m in markers]:
78+
return fixture_format != BlockchainFixture
79+
if "blockchain_test_engine_only" in [m.name for m in markers]:
80+
return fixture_format != BlockchainEngineFixture
81+
return False
82+
83+
def get_genesis_environment(self, fork: Fork) -> Environment:
84+
"""Get the genesis environment for this benchmark test."""
85+
return self.env
86+
87+
def split_transaction(self, tx: Transaction, gas_limit_cap: int | None) -> List[Transaction]:
88+
"""Split a transaction that exceeds the gas limit cap into multiple transactions."""
89+
if (gas_limit_cap is None) or (tx.gas_limit <= gas_limit_cap):
90+
return [tx]
91+
92+
total_gas = int(self.expected_benchmark_gas_used or self.env.gas_limit)
93+
print(f"total_gas: {total_gas}")
94+
num_splits = total_gas // gas_limit_cap
95+
96+
split_transactions = []
97+
for i in range(num_splits):
98+
split_tx = tx.model_copy()
99+
total_gas -= gas_limit_cap
100+
split_tx.gas_limit = HexNumber(total_gas if i == num_splits - 1 else gas_limit_cap)
101+
split_tx.nonce = HexNumber(tx.nonce + i)
102+
split_transactions.append(split_tx)
103+
104+
return split_transactions
105+
106+
def generate_blockchain_test(self, fork: Fork) -> BlockchainTest:
107+
"""Create a BlockchainTest from this BenchmarkTest."""
108+
if self.blocks is not None:
109+
return BlockchainTest.from_test(
110+
base_test=self,
111+
genesis_environment=self.env,
112+
pre=self.pre,
113+
post=self.post,
114+
blocks=self.blocks,
115+
)
116+
elif self.tx is not None:
117+
gas_limit_cap = fork.transaction_gas_limit_cap()
118+
119+
transactions = self.split_transaction(self.tx, gas_limit_cap)
120+
121+
blocks = [Block(txs=transactions)]
122+
123+
return BlockchainTest.from_test(
124+
base_test=self,
125+
pre=self.pre,
126+
post=self.post,
127+
blocks=blocks,
128+
genesis_environment=self.env,
129+
)
130+
else:
131+
raise ValueError("Cannot create BlockchainTest without transactions or blocks")
132+
133+
def generate(
134+
self,
135+
t8n: TransitionTool,
136+
fork: Fork,
137+
fixture_format: FixtureFormat,
138+
) -> BaseFixture:
139+
"""Generate the blockchain test fixture."""
140+
self.check_exception_test(exception=self.tx.error is not None if self.tx else False)
141+
if fixture_format in BlockchainTest.supported_fixture_formats:
142+
return self.generate_blockchain_test(fork=fork).generate(
143+
t8n=t8n, fork=fork, fixture_format=fixture_format
144+
)
145+
else:
146+
raise Exception(f"Unsupported fixture format: {fixture_format}")
147+
148+
def execute(
149+
self,
150+
*,
151+
fork: Fork,
152+
execute_format: ExecuteFormat,
153+
) -> BaseExecute:
154+
"""Execute the benchmark test by sending it to the live network."""
155+
if execute_format == TransactionPost:
156+
return TransactionPost(
157+
blocks=[[self.tx]],
158+
post=self.post,
159+
)
160+
raise Exception(f"Unsupported execute format: {execute_format}")
161+
162+
163+
BenchmarkTestSpec = Callable[[str], Generator[BenchmarkTest, None, None]]
164+
BenchmarkTestFiller = Type[BenchmarkTest]

src/ethereum_test_tools/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from ethereum_test_fixtures import BaseFixture, FixtureCollector
2626
from ethereum_test_specs import (
2727
BaseTest,
28+
BenchmarkTest,
29+
BenchmarkTestFiller,
2830
BlobsTest,
2931
BlobsTestFiller,
3032
BlockchainTest,
@@ -98,6 +100,8 @@
98100
"AuthorizationTuple",
99101
"BaseFixture",
100102
"BaseTest",
103+
"BenchmarkTest",
104+
"BenchmarkTestFiller",
101105
"Blob",
102106
"BlobsTest",
103107
"BlobsTestFiller",

tests/benchmark/test_worst_blocks.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
Account,
1616
Address,
1717
Alloc,
18+
BenchmarkTestFiller,
1819
Block,
19-
BlockchainTestFiller,
20+
Environment,
2021
Hash,
2122
StateTestFiller,
2223
Transaction,
@@ -111,8 +112,9 @@ def ether_transfer_case(
111112
["a_to_a", "a_to_b", "diff_acc_to_b", "a_to_diff_acc", "diff_acc_to_diff_acc"],
112113
)
113114
def test_block_full_of_ether_transfers(
114-
blockchain_test: BlockchainTestFiller,
115+
benchmark_test: BenchmarkTestFiller,
115116
pre: Alloc,
117+
env: Environment,
116118
case_id: str,
117119
ether_transfer_case,
118120
iteration_count: int,
@@ -153,7 +155,8 @@ def test_block_full_of_ether_transfers(
153155
else {receiver: Account(balance=balance) for receiver, balance in balances.items()}
154156
)
155157

156-
blockchain_test(
158+
benchmark_test(
159+
genesis_environment=env,
157160
pre=pre,
158161
post=post_state,
159162
blocks=[Block(txs=txs)],

0 commit comments

Comments
 (0)