Skip to content

Commit 0ef971d

Browse files
feat: wrap state test for benchmark
1 parent 7d77562 commit 0ef971d

File tree

4 files changed

+243
-2
lines changed

4 files changed

+243
-2
lines changed

src/ethereum_test_specs/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from .base import BaseTest, TestSpec
44
from .base_static import BaseStaticTest
55
from .benchmark import BenchmarkTest, BenchmarkTestFiller, BenchmarkTestSpec
6+
from .benchmark_state import BenchmarkStateTest, BenchmarkStateTestFiller, BenchmarkStateTestSpec
67
from .blobs import BlobsTest, BlobsTestFiller, BlobsTestSpec
78
from .blockchain import (
89
BlockchainTest,
@@ -27,6 +28,9 @@
2728
"BenchmarkTest",
2829
"BenchmarkTestFiller",
2930
"BenchmarkTestSpec",
31+
"BenchmarkStateTest",
32+
"BenchmarkStateTestFiller",
33+
"BenchmarkStateTestSpec",
3034
"BlobsTest",
3135
"BlobsTestFiller",
3236
"BlobsTestSpec",
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
"""Ethereum benchmark state test spec definition and filler."""
2+
3+
import math
4+
from pprint import pprint
5+
from typing import Callable, ClassVar, Generator, List, Sequence, Type
6+
7+
from pydantic import ConfigDict
8+
9+
from ethereum_clis import TransitionTool
10+
from ethereum_test_base_types import HexNumber
11+
from ethereum_test_execution import (
12+
BaseExecute,
13+
ExecuteFormat,
14+
LabeledExecuteFormat,
15+
TransactionPost,
16+
)
17+
from ethereum_test_fixtures import (
18+
BaseFixture,
19+
FixtureFormat,
20+
LabeledFixtureFormat,
21+
StateFixture,
22+
)
23+
from ethereum_test_fixtures.common import FixtureBlobSchedule
24+
from ethereum_test_fixtures.state import (
25+
FixtureConfig,
26+
FixtureEnvironment,
27+
FixtureForkPost,
28+
FixtureTransaction,
29+
)
30+
from ethereum_test_forks import Fork
31+
from ethereum_test_types import Alloc, Environment, Transaction
32+
from ethereum_test_vm import Bytecode
33+
34+
from .base import BaseTest, OpMode
35+
from .blockchain import Block, BlockchainTest
36+
from .debugging import print_traces
37+
from .helpers import verify_transactions
38+
39+
40+
class BenchmarkStateTest(BaseTest):
41+
"""Test type designed specifically for benchmark state test cases with full verification."""
42+
43+
pre: Alloc
44+
post: Alloc
45+
tx: Transaction
46+
gas_benchmark_value: int
47+
setup_bytecode: Bytecode | None = None
48+
attack_bytecode: Bytecode | None = None
49+
env: Environment
50+
chain_id: int = 1
51+
52+
model_config = ConfigDict(arbitrary_types_allowed=True)
53+
54+
supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [
55+
StateFixture,
56+
] + [
57+
LabeledFixtureFormat(
58+
fixture_format,
59+
f"{fixture_format.format_name}_from_benchmark_state_test",
60+
f"A {fixture_format.format_name} generated from a benchmark_state_test",
61+
)
62+
for fixture_format in BlockchainTest.supported_fixture_formats
63+
]
64+
65+
supported_execute_formats: ClassVar[Sequence[LabeledExecuteFormat]] = [
66+
LabeledExecuteFormat(
67+
TransactionPost,
68+
"benchmark_state_test_with_verification",
69+
"An execute test derived from a benchmark state test with verification",
70+
),
71+
]
72+
73+
def split_transaction(self, tx: Transaction, gas_limit_cap: int | None) -> List[Transaction]:
74+
"""Split a transaction that exceeds the gas limit cap into multiple transactions."""
75+
if (gas_limit_cap is None) or (tx.gas_limit <= gas_limit_cap):
76+
return [tx]
77+
78+
total_gas = int(tx.gas_limit)
79+
num_splits = math.ceil(total_gas / gas_limit_cap)
80+
81+
split_transactions = []
82+
remaining_gas = total_gas
83+
for i in range(num_splits):
84+
split_tx = tx.model_copy()
85+
split_tx.gas_limit = HexNumber(min(gas_limit_cap, remaining_gas))
86+
split_tx.nonce = HexNumber(tx.nonce + i)
87+
split_transactions.append(split_tx)
88+
remaining_gas -= gas_limit_cap
89+
90+
return split_transactions
91+
92+
def make_benchmark_state_test_fixture(
93+
self,
94+
t8n: TransitionTool,
95+
fork: Fork,
96+
) -> StateFixture:
97+
"""Create a fixture from the benchmark state test definition with full verification."""
98+
# We can't generate a state test fixture that names a transition fork,
99+
# so we get the fork at the block number and timestamp of the state test
100+
fork = fork.fork_at(self.env.number, self.env.timestamp)
101+
102+
env = self.env.set_fork_requirements(fork)
103+
tx = self.tx.with_signature_and_sender(keep_secret_key=True)
104+
pre_alloc = Alloc.merge(
105+
Alloc.model_validate(fork.pre_allocation()),
106+
self.pre,
107+
)
108+
109+
# Verification 1: Check for empty accounts
110+
if empty_accounts := pre_alloc.empty_accounts():
111+
raise Exception(f"Empty accounts in pre state: {empty_accounts}")
112+
113+
transition_tool_output = t8n.evaluate(
114+
transition_tool_data=TransitionTool.TransitionToolData(
115+
alloc=pre_alloc,
116+
txs=[tx],
117+
env=env,
118+
fork=fork,
119+
chain_id=self.chain_id,
120+
reward=0, # Reward on state tests is always zero
121+
blob_schedule=fork.blob_schedule(),
122+
state_test=True,
123+
),
124+
debug_output_path=self.get_next_transition_tool_output_path(),
125+
slow_request=self.is_tx_gas_heavy_test(),
126+
)
127+
128+
# Verification 2: Post-allocation verification
129+
try:
130+
self.post.verify_post_alloc(transition_tool_output.alloc)
131+
except Exception as e:
132+
print_traces(t8n.get_traces())
133+
raise e
134+
135+
# Verification 3: Transaction verification
136+
try:
137+
verify_transactions(
138+
txs=[tx],
139+
result=transition_tool_output.result,
140+
transition_tool_exceptions_reliable=t8n.exception_mapper.reliable,
141+
)
142+
except Exception as e:
143+
print_traces(t8n.get_traces())
144+
pprint(transition_tool_output.result)
145+
pprint(transition_tool_output.alloc)
146+
raise e
147+
148+
# Verification 4: Benchmark gas validation
149+
if self._operation_mode == OpMode.BENCHMARKING:
150+
expected_benchmark_gas_used = self.gas_benchmark_value
151+
gas_used = int(transition_tool_output.result.gas_used)
152+
assert expected_benchmark_gas_used is not None, "gas_benchmark_value is not set"
153+
assert gas_used == expected_benchmark_gas_used, (
154+
f"gas_used ({gas_used}) does not match gas_benchmark_value "
155+
f"({expected_benchmark_gas_used})"
156+
f", difference: {gas_used - expected_benchmark_gas_used}"
157+
)
158+
159+
return StateFixture(
160+
env=FixtureEnvironment(**env.model_dump(exclude_none=True)),
161+
pre=pre_alloc,
162+
post={
163+
fork: [
164+
FixtureForkPost(
165+
state_root=transition_tool_output.result.state_root,
166+
logs_hash=transition_tool_output.result.logs_hash,
167+
tx_bytes=tx.rlp(),
168+
expect_exception=tx.error,
169+
state=transition_tool_output.alloc,
170+
)
171+
]
172+
},
173+
transaction=FixtureTransaction.from_transaction(tx),
174+
config=FixtureConfig(
175+
blob_schedule=FixtureBlobSchedule.from_blob_schedule(fork.blob_schedule()),
176+
chain_id=self.chain_id,
177+
),
178+
)
179+
180+
def generate_blockchain_test(self, fork: Fork) -> BlockchainTest:
181+
"""Create a BlockchainTest from this BenchmarkStateTestWithVerification."""
182+
gas_limit_cap = fork.transaction_gas_limit_cap()
183+
184+
transactions = self.split_transaction(self.tx, gas_limit_cap)
185+
186+
blocks = [Block(txs=transactions)]
187+
188+
return BlockchainTest.from_test(
189+
base_test=self,
190+
pre=self.pre,
191+
post=self.post,
192+
blocks=blocks,
193+
genesis_environment=self.env,
194+
)
195+
196+
def generate(
197+
self,
198+
t8n: TransitionTool,
199+
fork: Fork,
200+
fixture_format: FixtureFormat,
201+
) -> BaseFixture:
202+
"""Generate the test fixture."""
203+
self.check_exception_test(exception=self.tx.error is not None)
204+
if fixture_format in BlockchainTest.supported_fixture_formats:
205+
return self.generate_blockchain_test(fork=fork).generate(
206+
t8n=t8n, fork=fork, fixture_format=fixture_format
207+
)
208+
elif fixture_format == StateFixture:
209+
return self.make_benchmark_state_test_fixture(t8n, fork)
210+
211+
raise Exception(f"Unknown fixture format: {fixture_format}")
212+
213+
def execute(
214+
self,
215+
*,
216+
fork: Fork,
217+
execute_format: ExecuteFormat,
218+
) -> BaseExecute:
219+
"""Execute the benchmark state test by sending it to the live network."""
220+
if execute_format == TransactionPost:
221+
return TransactionPost(
222+
blocks=[[self.tx]],
223+
post=self.post,
224+
)
225+
raise Exception(f"Unsupported execute format: {execute_format}")
226+
227+
228+
BenchmarkStateTestFiller = Type[BenchmarkStateTest]
229+
BenchmarkStateTestSpec = Callable[[str], Generator[BenchmarkStateTest, None, None]]

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+
BenchmarkStateTest,
29+
BenchmarkStateTestFiller,
2830
BenchmarkTest,
2931
BenchmarkTestFiller,
3032
BlobsTest,
@@ -102,6 +104,8 @@
102104
"BaseTest",
103105
"BenchmarkTest",
104106
"BenchmarkTestFiller",
107+
"BenchmarkStateTest",
108+
"BenchmarkStateTestFiller",
105109
"Blob",
106110
"BlobsTest",
107111
"BlobsTestFiller",

tests/benchmark/test_worst_compute.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from ethereum_test_tools import (
2020
Address,
2121
Alloc,
22+
BenchmarkStateTestFiller,
2223
Block,
2324
BlockchainTestFiller,
2425
Bytecode,
@@ -2761,8 +2762,9 @@ def test_worst_calldataload(
27612762
],
27622763
)
27632764
def test_worst_swap(
2764-
state_test: StateTestFiller,
2765+
benchmark_state_test: BenchmarkStateTestFiller,
27652766
pre: Alloc,
2767+
env: Environment,
27662768
fork: Fork,
27672769
opcode: Opcode,
27682770
gas_benchmark_value: int,
@@ -2782,8 +2784,10 @@ def test_worst_swap(
27822784
sender=pre.fund_eoa(),
27832785
)
27842786

2785-
state_test(
2787+
benchmark_state_test(
2788+
env=env,
27862789
pre=pre,
2790+
gas_benchmark_value=gas_benchmark_value,
27872791
post={},
27882792
tx=tx,
27892793
)

0 commit comments

Comments
 (0)