Skip to content

Commit de7f485

Browse files
feat(benchmark): add code generator to generate transaction
1 parent 0ef971d commit de7f485

File tree

5 files changed

+115
-20
lines changed

5 files changed

+115
-20
lines changed

src/ethereum_test_specs/benchmark.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ def generate_blockchain_test(self, fork: Fork) -> BlockchainTest:
128128
genesis_environment=self.env,
129129
)
130130
else:
131-
raise ValueError("Cannot create BlockchainTest without transactions or blocks")
131+
raise ValueError(
132+
"Cannot create BlockchainTest without transactions, blocks, or code_generator"
133+
)
132134

133135
def generate(
134136
self,

src/ethereum_test_specs/benchmark_state.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
)
3030
from ethereum_test_forks import Fork
3131
from ethereum_test_types import Alloc, Environment, Transaction
32-
from ethereum_test_vm import Bytecode
3332

3433
from .base import BaseTest, OpMode
3534
from .blockchain import Block, BlockchainTest
@@ -44,8 +43,6 @@ class BenchmarkStateTest(BaseTest):
4443
post: Alloc
4544
tx: Transaction
4645
gas_benchmark_value: int
47-
setup_bytecode: Bytecode | None = None
48-
attack_bytecode: Bytecode | None = None
4946
env: Environment
5047
chain_id: int = 1
5148

src/ethereum_test_tools/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
call_return_code,
7979
)
8080

81+
from .benchmark_code_generator import (
82+
BenchmarkCodeGenerator,
83+
ExtCallGenerator,
84+
JumpLoopGenerator,
85+
)
8186
from .code import (
8287
CalldataCase,
8388
Case,
@@ -102,6 +107,7 @@
102107
"AuthorizationTuple",
103108
"BaseFixture",
104109
"BaseTest",
110+
"BenchmarkCodeGenerator",
105111
"BenchmarkTest",
106112
"BenchmarkTestFiller",
107113
"BenchmarkStateTest",
@@ -120,6 +126,7 @@
120126
"CodeGasMeasure",
121127
"Conditional",
122128
"ConsolidationRequest",
129+
"ExtCallGenerator",
123130
"DeploymentTestType",
124131
"DepositRequest",
125132
"EngineAPIError",
@@ -135,6 +142,7 @@
135142
"Hash",
136143
"Header",
137144
"Initcode",
145+
"JumpLoopGenerator",
138146
"Macro",
139147
"Macros",
140148
"NetworkWrappedTransaction",
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Benchmark code generator classes for creating optimized bytecode patterns."""
2+
3+
from abc import ABC, abstractmethod
4+
from typing import Optional
5+
6+
from ethereum_test_forks import Fork
7+
from ethereum_test_tools import Alloc, Bytecode, Transaction
8+
from ethereum_test_tools.vm.opcode import Opcodes as Op
9+
10+
11+
class BenchmarkCodeGenerator(ABC):
12+
"""Abstract base class for generating benchmark bytecode."""
13+
14+
def __init__(
15+
self,
16+
fork: Fork,
17+
attack_block: Bytecode,
18+
setup: Optional[Bytecode] = None,
19+
):
20+
"""Initialize with fork, attack block, and optional setup bytecode."""
21+
self.fork = fork
22+
self.setup = setup or Bytecode()
23+
self.attack_block = attack_block
24+
25+
@abstractmethod
26+
def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction:
27+
"""Generate a transaction with the specified gas limit."""
28+
pass
29+
30+
def generate_repeated_code(self, repeated_code: Bytecode, setup: Bytecode) -> Bytecode:
31+
"""Calculate the maximum number of iterations that can fit in the code size limit."""
32+
max_code_size = self.fork.max_code_size()
33+
34+
overhead = len(Op.JUMPDEST) + len(Op.JUMP(len(setup)))
35+
available_space = max_code_size - overhead
36+
max_iterations = available_space // len(repeated_code) if len(repeated_code) > 0 else 0
37+
38+
code = setup + Op.JUMPDEST + repeated_code * max_iterations + Op.JUMP(len(setup))
39+
40+
self._validate_code_size(code)
41+
42+
return code
43+
44+
def _validate_code_size(self, code: Bytecode) -> None:
45+
"""Validate that the generated code fits within size limits."""
46+
if len(code) > self.fork.max_code_size():
47+
raise ValueError(
48+
f"Generated code size {len(code)} exceeds maximum allowed size "
49+
f"{self.fork.max_code_size()}"
50+
)
51+
52+
53+
class JumpLoopGenerator(BenchmarkCodeGenerator):
54+
"""Generates bytecode that loops execution using JUMP operations."""
55+
56+
def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction:
57+
"""Generate transaction with looping bytecode pattern."""
58+
# Benchmark Test Structure:
59+
# setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_length)
60+
61+
code = self.generate_repeated_code(self.attack_block, self.setup)
62+
63+
return Transaction(
64+
to=pre.deploy_contract(code=code),
65+
gas_limit=self.fork.transaction_gas_limit_cap() or 30_000_000,
66+
sender=pre.fund_eoa(),
67+
)
68+
69+
70+
class ExtCallGenerator(BenchmarkCodeGenerator):
71+
"""Generates bytecode that fills the contract to maximum allowed code size."""
72+
73+
def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction:
74+
"""Generate transaction with maximal code size coverage."""
75+
# Benchmark Test Structure:
76+
# There are two contracts:
77+
# 1. The target contract that executes certain operation but not loop (e.g. PUSH)
78+
# 2. The loop contract that calls the target contract in a loop
79+
#
80+
# attack = POP(STATICCALL(GAS, target_contract_address, 0, 0, 0, 0))
81+
# setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_lengt)
82+
# This could optimize the gas consumption and increase the cycle count.
83+
84+
max_stack_height = self.fork.max_stack_height()
85+
86+
target_contract_address = pre.deploy_contract(code=self.attack_block * max_stack_height)
87+
88+
code_sequence = Op.POP(Op.STATICCALL(Op.GAS, target_contract_address, 0, 0, 0, 0))
89+
90+
code = self.generate_repeated_code(code_sequence, Bytecode())
91+
92+
return Transaction(
93+
to=pre.deploy_contract(code=code),
94+
gas_limit=self.fork.transaction_gas_limit_cap() or 30_000_000,
95+
sender=pre.fund_eoa(),
96+
)

tests/benchmark/test_worst_compute.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
Transaction,
2929
add_kzg_version,
3030
)
31+
from ethereum_test_tools.benchmark_code_generator import JumpLoopGenerator
3132
from ethereum_test_tools.vm.opcode import Opcodes as Op
3233
from ethereum_test_types import TransactionType
3334
from ethereum_test_vm.opcode import Opcode
@@ -1829,27 +1830,19 @@ def test_worst_jumpis(
18291830
@pytest.mark.valid_from("Cancun")
18301831
@pytest.mark.slow
18311832
def test_worst_jumpdests(
1832-
state_test: StateTestFiller,
1833+
benchmark_state_test: BenchmarkStateTestFiller,
18331834
pre: Alloc,
1835+
env: Environment,
18341836
fork: Fork,
18351837
gas_benchmark_value: int,
18361838
):
18371839
"""Test running a JUMPDEST-intensive contract."""
1838-
max_code_size = fork.max_code_size()
1840+
generator = JumpLoopGenerator(fork, Op.JUMPDEST)
1841+
tx = generator.generate_transaction(pre, gas_benchmark_value)
18391842

1840-
# Create and deploy a contract with many JUMPDESTs
1841-
code_suffix = Op.JUMP(Op.PUSH0)
1842-
code_body = Op.JUMPDEST * (max_code_size - len(code_suffix))
1843-
code = code_body + code_suffix
1844-
jumpdests_address = pre.deploy_contract(code=code)
1845-
1846-
tx = Transaction(
1847-
to=jumpdests_address,
1848-
gas_limit=gas_benchmark_value,
1849-
sender=pre.fund_eoa(),
1850-
)
1851-
1852-
state_test(
1843+
benchmark_state_test(
1844+
env=env,
1845+
gas_benchmark_value=gas_benchmark_value,
18531846
pre=pre,
18541847
post={},
18551848
tx=tx,
@@ -2780,7 +2773,6 @@ def test_worst_swap(
27802773

27812774
tx = Transaction(
27822775
to=pre.deploy_contract(code=code),
2783-
gas_limit=gas_benchmark_value,
27842776
sender=pre.fund_eoa(),
27852777
)
27862778

0 commit comments

Comments
 (0)