5
5
Tests that benchmark EVMs in the worst-case memory opcodes.
6
6
"""
7
7
8
+ from enum import auto
9
+
8
10
import pytest
9
11
10
12
from ethereum_test_base_types .base_types import Bytes
13
+ from ethereum_test_benchmark .benchmark_code_generator import JumpLoopGenerator
11
14
from ethereum_test_forks import Fork
12
15
from ethereum_test_tools import (
13
16
Alloc ,
17
+ BenchmarkTestFiller ,
14
18
Bytecode ,
15
- StateTestFiller ,
16
19
Transaction ,
17
20
)
18
21
from ethereum_test_vm import Opcodes as Op
19
22
20
- from .helpers import code_loop_precompile_call
21
-
22
23
REFERENCE_SPEC_GIT_PATH = "TODO"
23
24
REFERENCE_SPEC_VERSION = "TODO"
24
25
25
26
26
27
class CallDataOrigin :
27
28
"""Enum for calldata origins."""
28
29
29
- TRANSACTION = 1
30
- CALL = 2
30
+ TRANSACTION = auto ()
31
+ CALL = auto ()
31
32
32
33
33
34
@pytest .mark .parametrize (
@@ -61,7 +62,7 @@ class CallDataOrigin:
61
62
],
62
63
)
63
64
def test_worst_calldatacopy (
64
- state_test : StateTestFiller ,
65
+ benchmark_test : BenchmarkTestFiller ,
65
66
pre : Alloc ,
66
67
fork : Fork ,
67
68
origin : CallDataOrigin ,
@@ -87,13 +88,15 @@ def test_worst_calldatacopy(
87
88
#
88
89
# If `non_zero_data` is True, we leverage CALLDATASIZE for the copy length. Otherwise, since we
89
90
# don't send zero data explicitly via calldata, PUSH the target size and use DUP1 to copy it.
90
- prefix = Bytecode () if non_zero_data or size == 0 else Op .PUSH3 (size )
91
+ setup = Bytecode () if non_zero_data or size == 0 else Op .PUSH3 (size )
91
92
src_dst = 0 if fixed_src_dst else Op .MOD (Op .GAS , 7 )
92
93
attack_block = Op .CALLDATACOPY (
93
94
src_dst , src_dst , Op .CALLDATASIZE if non_zero_data or size == 0 else Op .DUP1
94
95
)
95
- code = code_loop_precompile_call (prefix , attack_block , fork )
96
- code_address = pre .deploy_contract (code = code )
96
+
97
+ code_address = JumpLoopGenerator (setup = setup , attack_block = attack_block ).deploy_contracts (
98
+ pre , fork
99
+ )
97
100
98
101
tx_target = code_address
99
102
@@ -102,15 +105,17 @@ def test_worst_calldatacopy(
102
105
if origin == CallDataOrigin .CALL :
103
106
# If `non_zero_data` is False we leverage just using zeroed memory. Otherwise, we
104
107
# copy the calldata received from the transaction.
105
- prefix = (
108
+ setup = (
106
109
Op .CALLDATACOPY (Op .PUSH0 , Op .PUSH0 , Op .CALLDATASIZE ) if non_zero_data else Bytecode ()
107
110
) + Op .JUMPDEST
108
111
arg_size = Op .CALLDATASIZE if non_zero_data else size
109
- code = prefix + Op .STATICCALL (
112
+ attack_block = Op .STATICCALL (
110
113
address = code_address , args_offset = Op .PUSH0 , args_size = arg_size
111
114
)
112
- code += Op .JUMP (len (prefix ) - 1 )
113
- tx_target = pre .deploy_contract (code = code )
115
+
116
+ tx_target = JumpLoopGenerator (setup = setup , attack_block = attack_block ).deploy_contracts (
117
+ pre , fork
118
+ )
114
119
115
120
tx = Transaction (
116
121
to = tx_target ,
@@ -119,7 +124,7 @@ def test_worst_calldatacopy(
119
124
sender = pre .fund_eoa (),
120
125
)
121
126
122
- state_test (
127
+ benchmark_test (
123
128
pre = pre ,
124
129
post = {},
125
130
tx = tx ,
@@ -144,38 +149,39 @@ def test_worst_calldatacopy(
144
149
],
145
150
)
146
151
def test_worst_codecopy (
147
- state_test : StateTestFiller ,
152
+ benchmark_test : BenchmarkTestFiller ,
148
153
pre : Alloc ,
149
154
fork : Fork ,
150
155
max_code_size_ratio : float ,
151
156
fixed_src_dst : bool ,
152
- gas_benchmark_value : int ,
153
157
):
154
158
"""Test running a block filled with CODECOPY executions."""
155
159
max_code_size = fork .max_code_size ()
156
160
157
161
size = int (max_code_size * max_code_size_ratio )
158
162
159
- code_prefix = Op .PUSH32 (size )
163
+ setup = Op .PUSH32 (size )
160
164
src_dst = 0 if fixed_src_dst else Op .MOD (Op .GAS , 7 )
161
165
attack_block = Op .CODECOPY (src_dst , src_dst , Op .DUP1 ) # DUP1 copies size.
162
- code = code_loop_precompile_call (code_prefix , attack_block , fork )
166
+
167
+ code = JumpLoopGenerator (setup = setup , attack_block = attack_block ).generate_repeated_code (
168
+ attack_block , Bytecode (), Bytecode (), fork
169
+ )
163
170
164
171
# The code generated above is not guaranteed to be of max_code_size, so we pad it since
165
172
# a test parameter targets CODECOPYing a contract with max code size. Padded bytecode values
166
173
# are not relevant.
167
- code = code + Op .INVALID * (max_code_size - len (code ))
174
+ code += Op .INVALID * (max_code_size - len (code ))
168
175
assert len (code ) == max_code_size , (
169
176
f"Code size { len (code )} is not equal to max code size { max_code_size } ."
170
177
)
171
178
172
179
tx = Transaction (
173
180
to = pre .deploy_contract (code = code ),
174
- gas_limit = gas_benchmark_value ,
175
181
sender = pre .fund_eoa (),
176
182
)
177
183
178
- state_test (
184
+ benchmark_test (
179
185
pre = pre ,
180
186
post = {},
181
187
tx = tx ,
@@ -199,16 +205,12 @@ def test_worst_codecopy(
199
205
],
200
206
)
201
207
def test_worst_returndatacopy (
202
- state_test : StateTestFiller ,
208
+ benchmark_test : BenchmarkTestFiller ,
203
209
pre : Alloc ,
204
- fork : Fork ,
205
210
size : int ,
206
211
fixed_dst : bool ,
207
- gas_benchmark_value : int ,
208
212
):
209
213
"""Test running a block filled with RETURNDATACOPY executions."""
210
- max_code_size = fork .max_code_size ()
211
-
212
214
# Create the contract that will RETURN the data that will be used for RETURNDATACOPY.
213
215
# Random-ish data is injected at different points in memory to avoid making the content
214
216
# predictable. If `size` is 0, this helper contract won't be used.
@@ -220,13 +222,13 @@ def test_worst_returndatacopy(
220
222
)
221
223
helper_contract = pre .deploy_contract (code = code )
222
224
223
- # We create the contract that will be doing the RETURNDATACOPY multiple times.
224
225
returndata_gen = Op .STATICCALL (address = helper_contract ) if size > 0 else Bytecode ()
225
226
dst = 0 if fixed_dst else Op .MOD (Op .GAS , 7 )
226
- attack_iter = Op .RETURNDATACOPY (dst , Op .PUSH0 , Op .RETURNDATASIZE )
227
227
228
- jumpdest = Op .JUMPDEST
229
- jump_back = Op .JUMP (len (returndata_gen ))
228
+ # We create the contract that will be doing the RETURNDATACOPY multiple times.
229
+ returndata_gen = Op .STATICCALL (address = helper_contract ) if size > 0 else Bytecode ()
230
+ attack_block = Op .RETURNDATACOPY (dst , Op .PUSH0 , Op .RETURNDATASIZE )
231
+
230
232
# The attack loop is constructed as:
231
233
# ```
232
234
# JUMPDEST(#)
@@ -238,30 +240,13 @@ def test_worst_returndatacopy(
238
240
# ```
239
241
# The goal is that once per (big) loop iteration, the helper contract is called to
240
242
# generate fresh returndata to continue calling RETURNDATACOPY.
241
- max_iters_loop = (
242
- max_code_size - 2 * len (returndata_gen ) - len (jumpdest ) - len (jump_back )
243
- ) // len (attack_iter )
244
- code = (
245
- returndata_gen
246
- + jumpdest
247
- + sum ([attack_iter ] * max_iters_loop )
248
- + returndata_gen
249
- + jump_back
250
- )
251
- assert len (code ) <= max_code_size , (
252
- f"Code size { len (code )} is not equal to max code size { max_code_size } ."
253
- )
254
-
255
- tx = Transaction (
256
- to = pre .deploy_contract (code = code ),
257
- gas_limit = gas_benchmark_value ,
258
- sender = pre .fund_eoa (),
259
- )
260
243
261
- state_test (
244
+ benchmark_test (
262
245
pre = pre ,
263
246
post = {},
264
- tx = tx ,
247
+ code_generator = JumpLoopGenerator (
248
+ setup = returndata_gen , attack_block = attack_block , cleanup = returndata_gen
249
+ ),
265
250
)
266
251
267
252
@@ -282,42 +267,24 @@ def test_worst_returndatacopy(
282
267
],
283
268
)
284
269
def test_worst_mcopy (
285
- state_test : StateTestFiller ,
270
+ benchmark_test : BenchmarkTestFiller ,
286
271
pre : Alloc ,
287
- fork : Fork ,
288
272
size : int ,
289
273
fixed_src_dst : bool ,
290
- gas_benchmark_value : int ,
291
274
):
292
275
"""Test running a block filled with MCOPY executions."""
293
- max_code_size = fork .max_code_size ()
276
+ src_dst = 0 if fixed_src_dst else Op .MOD (Op .GAS , 7 )
277
+ attack_block = Op .MCOPY (src_dst , src_dst , size )
294
278
295
279
mem_touch = (
296
280
Op .MSTORE8 (0 , Op .GAS ) + Op .MSTORE8 (size // 2 , Op .GAS ) + Op .MSTORE8 (size - 1 , Op .GAS )
297
281
if size > 0
298
282
else Bytecode ()
299
283
)
300
- src_dst = 0 if fixed_src_dst else Op .MOD (Op .GAS , 7 )
301
- attack_block = Op .MCOPY (src_dst , src_dst , size )
302
-
303
- jumpdest = Op .JUMPDEST
304
- jump_back = Op .JUMP (len (mem_touch ))
305
- max_iters_loop = (max_code_size - 2 * len (mem_touch ) - len (jumpdest ) - len (jump_back )) // len (
306
- attack_block
307
- )
308
- code = mem_touch + jumpdest + sum ([attack_block ] * max_iters_loop ) + mem_touch + jump_back
309
- assert len (code ) <= max_code_size , (
310
- f"Code size { len (code )} is not equal to max code size { max_code_size } ."
311
- )
312
-
313
- tx = Transaction (
314
- to = pre .deploy_contract (code = code ),
315
- gas_limit = gas_benchmark_value ,
316
- sender = pre .fund_eoa (),
317
- )
318
-
319
- state_test (
284
+ benchmark_test (
320
285
pre = pre ,
321
286
post = {},
322
- tx = tx ,
287
+ code_generator = JumpLoopGenerator (
288
+ setup = mem_touch , attack_block = attack_block , cleanup = mem_touch
289
+ ),
323
290
)
0 commit comments