Skip to content

Commit 131adfe

Browse files
refactor(benchmark): update to benchmark test wrapper (#2160)
* refactor(benchmark): update code generator interface * refactor(benchmark): update worst bytecode scenario * refactor(benchmark): update worst compute scenario * refactor(benchmark): update worst memory scenario * refactor(benchmark): update worst opcode scenario * refactor(benchmark): update worst statefule scenario * refactor(benchmark): update modexp cases by parameterization * refactor: remove blockchain filler in worst block cases * refactor: update blockhash case * fix: resolve failing blob and block has tests * fix: update linting comment length * fix linting and typing issue * fix incorrect logic for missing coverage * refactor unop worst case * refactor(tests/benchmark): Optimize generators usages * refactor(benchmark): Improve max_iterations calculation * feat(benchmark): add setup_blocks and update tests * fix(specs): Make sure setup blocks are always used --------- Co-authored-by: Mario Vega <[email protected]>
1 parent 67f596d commit 131adfe

File tree

8 files changed

+872
-1963
lines changed

8 files changed

+872
-1963
lines changed

src/ethereum_test_benchmark/benchmark_code_generator.py

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,62 @@
33
optimized bytecode patterns.
44
"""
55

6+
from dataclasses import dataclass
7+
8+
from ethereum_test_base_types import Address
69
from ethereum_test_forks import Fork
710
from ethereum_test_specs.benchmark import BenchmarkCodeGenerator
8-
from ethereum_test_types import Alloc, Transaction
9-
from ethereum_test_vm import Bytecode
11+
from ethereum_test_types import Alloc
1012
from ethereum_test_vm.opcodes import Opcodes as Op
1113

1214

15+
@dataclass(kw_only=True)
1316
class JumpLoopGenerator(BenchmarkCodeGenerator):
1417
"""Generates bytecode that loops execution using JUMP operations."""
1518

16-
def deploy_contracts(self, pre: Alloc, fork: Fork) -> None:
19+
def deploy_contracts(self, *, pre: Alloc, fork: Fork) -> Address:
1720
"""Deploy the looping contract."""
1821
# Benchmark Test Structure:
19-
# setup + JUMPDEST + attack + attack + ... +
20-
# attack + JUMP(setup_length)
21-
code = self.generate_repeated_code(self.attack_block, self.setup, fork)
22-
self._contract_address = pre.deploy_contract(code=code)
23-
24-
def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction:
25-
"""Generate transaction that executes the looping contract."""
26-
if not hasattr(self, "_contract_address"):
27-
raise ValueError("deploy_contracts must be called before generate_transaction")
28-
29-
return Transaction(
30-
to=self._contract_address,
31-
gas_limit=gas_limit,
32-
sender=pre.fund_eoa(),
22+
# setup + JUMPDEST +
23+
# attack + attack + ... + attack +
24+
# cleanup + JUMP(setup_length)
25+
code = self.generate_repeated_code(
26+
repeated_code=self.attack_block, setup=self.setup, cleanup=self.cleanup, fork=fork
3327
)
28+
self._contract_address = pre.deploy_contract(code=code)
29+
return self._contract_address
3430

3531

32+
@dataclass(kw_only=True)
3633
class ExtCallGenerator(BenchmarkCodeGenerator):
3734
"""
3835
Generates bytecode that fills the contract to
3936
maximum allowed code size.
4037
"""
4138

42-
def deploy_contracts(self, pre: Alloc, fork: Fork) -> None:
39+
contract_balance: int = 0
40+
41+
def deploy_contracts(self, *, pre: Alloc, fork: Fork) -> Address:
4342
"""Deploy both target and caller contracts."""
4443
# Benchmark Test Structure:
4544
# There are two contracts:
4645
# 1. The target contract that executes certain operation
4746
# but not loop (e.g. PUSH)
4847
# 2. The loop contract that calls the target contract in a loop
4948

50-
max_iterations = min(
51-
fork.max_stack_height(), fork.max_code_size() // len(self.attack_block)
52-
)
49+
pushed_stack_items = self.attack_block.pushed_stack_items
50+
popped_stack_items = self.attack_block.popped_stack_items
51+
stack_delta = pushed_stack_items - popped_stack_items
52+
53+
max_iterations = fork.max_code_size() // len(self.attack_block)
54+
55+
if stack_delta > 0:
56+
max_iterations = min(fork.max_stack_height() // stack_delta, max_iterations)
5357

5458
# Deploy target contract that contains the actual attack block
5559
self._target_contract_address = pre.deploy_contract(
56-
code=self.attack_block * max_iterations
60+
code=self.setup + self.attack_block * max_iterations,
61+
balance=self.contract_balance,
5762
)
5863

5964
# Create caller contract that repeatedly calls the target contract
@@ -65,16 +70,8 @@ def deploy_contracts(self, pre: Alloc, fork: Fork) -> None:
6570
# JUMP(setup_length)
6671
code_sequence = Op.POP(Op.STATICCALL(Op.GAS, self._target_contract_address, 0, 0, 0, 0))
6772

68-
caller_code = self.generate_repeated_code(code_sequence, Bytecode(), fork)
69-
self._contract_address = pre.deploy_contract(code=caller_code)
70-
71-
def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction:
72-
"""Generate transaction that executes the caller contract."""
73-
if not hasattr(self, "_contract_address"):
74-
raise ValueError("deploy_contracts must be called before generate_transaction")
75-
76-
return Transaction(
77-
to=self._contract_address,
78-
gas_limit=gas_limit,
79-
sender=pre.fund_eoa(),
73+
caller_code = self.generate_repeated_code(
74+
repeated_code=code_sequence, cleanup=self.cleanup, fork=fork
8075
)
76+
self._contract_address = pre.deploy_contract(code=caller_code)
77+
return self._contract_address

src/ethereum_test_specs/benchmark.py

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import math
44
from abc import ABC, abstractmethod
55
from dataclasses import dataclass, field
6-
from typing import Callable, ClassVar, Dict, Generator, List, Sequence, Type
6+
from typing import Any, Callable, ClassVar, Dict, Generator, List, Sequence, Type
77

88
import pytest
99
from pydantic import ConfigDict, Field
1010

1111
from ethereum_clis import TransitionTool
12-
from ethereum_test_base_types import HexNumber
12+
from ethereum_test_base_types import Address, HexNumber
1313
from ethereum_test_exceptions import BlockException, TransactionException
1414
from ethereum_test_execution import (
1515
BaseExecute,
@@ -40,32 +40,52 @@ class BenchmarkCodeGenerator(ABC):
4040

4141
attack_block: Bytecode
4242
setup: Bytecode = field(default_factory=Bytecode)
43+
cleanup: Bytecode = field(default_factory=Bytecode)
44+
tx_kwargs: Dict[str, Any] = field(default_factory=dict)
45+
_contract_address: Address | None = None
4346

4447
@abstractmethod
45-
def deploy_contracts(self, pre: Alloc, fork: Fork) -> None:
48+
def deploy_contracts(self, *, pre: Alloc, fork: Fork) -> Address:
4649
"""Deploy any contracts needed for the benchmark."""
4750
...
4851

49-
@abstractmethod
50-
def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction:
51-
"""Generate a transaction with the specified gas limit."""
52-
...
52+
def generate_transaction(self, *, pre: Alloc, gas_benchmark_value: int) -> Transaction:
53+
"""Generate transaction that executes the looping contract."""
54+
assert self._contract_address is not None
55+
if "gas_limit" not in self.tx_kwargs:
56+
self.tx_kwargs["gas_limit"] = gas_benchmark_value
57+
58+
return Transaction(
59+
to=self._contract_address,
60+
sender=pre.fund_eoa(),
61+
**self.tx_kwargs,
62+
)
5363

5464
def generate_repeated_code(
55-
self, repeated_code: Bytecode, setup: Bytecode, fork: Fork
65+
self,
66+
*,
67+
repeated_code: Bytecode,
68+
setup: Bytecode | None = None,
69+
cleanup: Bytecode | None = None,
70+
fork: Fork,
5671
) -> Bytecode:
5772
"""
5873
Calculate the maximum number of iterations that
5974
can fit in the code size limit.
6075
"""
6176
assert len(repeated_code) > 0, "repeated_code cannot be empty"
6277
max_code_size = fork.max_code_size()
63-
64-
overhead = len(setup) + len(Op.JUMPDEST) + len(Op.JUMP(len(setup)))
78+
if setup is None:
79+
setup = Bytecode()
80+
if cleanup is None:
81+
cleanup = Bytecode()
82+
overhead = len(setup) + len(Op.JUMPDEST) + len(cleanup) + len(Op.JUMP(len(setup)))
6583
available_space = max_code_size - overhead
6684
max_iterations = available_space // len(repeated_code)
6785

68-
code = setup + Op.JUMPDEST + repeated_code * max_iterations + Op.JUMP(len(setup))
86+
# TODO: Unify the PUSH0 and PUSH1 usage.
87+
code = setup + Op.JUMPDEST + repeated_code * max_iterations + cleanup
88+
code += Op.JUMP(len(setup)) if len(setup) > 0 else Op.PUSH0 + Op.JUMP
6989
self._validate_code_size(code, fork)
7090

7191
return code
@@ -84,9 +104,10 @@ class BenchmarkTest(BaseTest):
84104

85105
model_config = ConfigDict(extra="forbid")
86106

87-
pre: Alloc
107+
pre: Alloc = Field(default_factory=Alloc)
88108
post: Alloc = Field(default_factory=Alloc)
89109
tx: Transaction | None = None
110+
setup_blocks: List[Block] = Field(default_factory=list)
90111
blocks: List[Block] | None = None
91112
block_exception: (
92113
List[TransactionException | BlockException] | TransactionException | BlockException | None
@@ -115,6 +136,14 @@ class BenchmarkTest(BaseTest):
115136
"blockchain_test_only": "Only generate a blockchain test fixture",
116137
}
117138

139+
def model_post_init(self, __context: Any, /) -> None:
140+
"""
141+
Model post-init to assert that the custom pre-allocation was
142+
provided and the default was not used.
143+
"""
144+
super().model_post_init(__context)
145+
assert "pre" in self.model_fields_set, "pre allocation was not provided"
146+
118147
@classmethod
119148
def pytest_parameter_name(cls) -> str:
120149
"""
@@ -178,9 +207,11 @@ def generate_blocks_from_code_generator(self, fork: Fork) -> List[Block]:
178207
if self.code_generator is None:
179208
raise Exception("Code generator is not set")
180209

181-
self.code_generator.deploy_contracts(self.pre, fork)
210+
self.code_generator.deploy_contracts(pre=self.pre, fork=fork)
182211
gas_limit = fork.transaction_gas_limit_cap() or self.gas_benchmark_value
183-
benchmark_tx = self.code_generator.generate_transaction(self.pre, gas_limit)
212+
benchmark_tx = self.code_generator.generate_transaction(
213+
pre=self.pre, gas_benchmark_value=gas_limit
214+
)
184215

185216
execution_txs = self.split_transaction(benchmark_tx, gas_limit)
186217
execution_block = Block(txs=execution_txs)
@@ -204,39 +235,34 @@ def generate_blockchain_test(self, fork: Fork) -> BlockchainTest:
204235
f"Exactly one must be set, but got {len(set_props)}: {', '.join(set_props)}"
205236
)
206237

238+
blocks: List[Block] = self.setup_blocks
239+
207240
if self.code_generator is not None:
208241
generated_blocks = self.generate_blocks_from_code_generator(fork)
209-
return BlockchainTest.from_test(
210-
base_test=self,
211-
genesis_environment=self.env,
212-
pre=self.pre,
213-
post=self.post,
214-
blocks=generated_blocks,
215-
)
242+
blocks += generated_blocks
243+
216244
elif self.blocks is not None:
217-
return BlockchainTest.from_test(
218-
base_test=self,
219-
genesis_environment=self.env,
220-
pre=self.pre,
221-
post=self.post,
222-
blocks=self.blocks,
223-
)
245+
blocks += self.blocks
246+
224247
elif self.tx is not None:
225248
gas_limit = fork.transaction_gas_limit_cap() or self.gas_benchmark_value
226249

227250
transactions = self.split_transaction(self.tx, gas_limit)
228251

229-
blocks = [Block(txs=transactions)]
252+
blocks.append(Block(txs=transactions))
230253

231-
return BlockchainTest.from_test(
232-
base_test=self,
233-
pre=self.pre,
234-
post=self.post,
235-
blocks=blocks,
236-
genesis_environment=self.env,
237-
)
238254
else:
239-
raise ValueError("Cannot create BlockchainTest without transactions or blocks")
255+
raise ValueError(
256+
"Cannot create BlockchainTest without a code generator, transactions, or blocks"
257+
)
258+
259+
return BlockchainTest.from_test(
260+
base_test=self,
261+
genesis_environment=self.env,
262+
pre=self.pre,
263+
post=self.post,
264+
blocks=blocks,
265+
)
240266

241267
def generate(
242268
self,

0 commit comments

Comments
 (0)