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_tools .vm .opcode 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,40 @@ 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 ,
157
+ dq ,
153
158
):
154
159
"""Test running a block filled with CODECOPY executions."""
155
160
max_code_size = fork .max_code_size ()
156
161
157
162
size = int (max_code_size * max_code_size_ratio )
158
163
159
- code_prefix = Op .PUSH32 (size )
164
+ setup = Op .PUSH32 (size )
160
165
src_dst = 0 if fixed_src_dst else Op .MOD (Op .GAS , 7 )
161
166
attack_block = Op .CODECOPY (src_dst , src_dst , Op .DUP1 ) # DUP1 copies size.
162
- code = code_loop_precompile_call (code_prefix , attack_block , fork )
167
+
168
+ code = JumpLoopGenerator (setup = setup , attack_block = attack_block ).generate_repeated_code (
169
+ attack_block , Bytecode (), Bytecode (), fork
170
+ )
163
171
164
172
# The code generated above is not guaranteed to be of max_code_size, so we pad it since
165
173
# a test parameter targets CODECOPYing a contract with max code size. Padded bytecode values
166
174
# are not relevant.
167
- code = code + Op .INVALID * (max_code_size - len (code ))
175
+ code += Op .INVALID * (max_code_size - len (code ))
168
176
assert len (code ) == max_code_size , (
169
177
f"Code size { len (code )} is not equal to max code size { max_code_size } ."
170
178
)
171
179
172
180
tx = Transaction (
173
181
to = pre .deploy_contract (code = code ),
174
- gas_limit = gas_benchmark_value ,
175
182
sender = pre .fund_eoa (),
176
183
)
177
184
178
- state_test (
185
+ benchmark_test (
179
186
pre = pre ,
180
187
post = {},
181
188
tx = tx ,
@@ -199,16 +206,12 @@ def test_worst_codecopy(
199
206
],
200
207
)
201
208
def test_worst_returndatacopy (
202
- state_test : StateTestFiller ,
209
+ benchmark_test : BenchmarkTestFiller ,
203
210
pre : Alloc ,
204
- fork : Fork ,
205
211
size : int ,
206
212
fixed_dst : bool ,
207
- gas_benchmark_value : int ,
208
213
):
209
214
"""Test running a block filled with RETURNDATACOPY executions."""
210
- max_code_size = fork .max_code_size ()
211
-
212
215
# Create the contract that will RETURN the data that will be used for RETURNDATACOPY.
213
216
# Random-ish data is injected at different points in memory to avoid making the content
214
217
# predictable. If `size` is 0, this helper contract won't be used.
@@ -220,13 +223,13 @@ def test_worst_returndatacopy(
220
223
)
221
224
helper_contract = pre .deploy_contract (code = code )
222
225
223
- # We create the contract that will be doing the RETURNDATACOPY multiple times.
224
226
returndata_gen = Op .STATICCALL (address = helper_contract ) if size > 0 else Bytecode ()
225
227
dst = 0 if fixed_dst else Op .MOD (Op .GAS , 7 )
226
- attack_iter = Op .RETURNDATACOPY (dst , Op .PUSH0 , Op .RETURNDATASIZE )
227
228
228
- jumpdest = Op .JUMPDEST
229
- jump_back = Op .JUMP (len (returndata_gen ))
229
+ # We create the contract that will be doing the RETURNDATACOPY multiple times.
230
+ returndata_gen = Op .STATICCALL (address = helper_contract ) if size > 0 else Bytecode ()
231
+ attack_block = Op .RETURNDATACOPY (dst , Op .PUSH0 , Op .RETURNDATASIZE )
232
+
230
233
# The attack loop is constructed as:
231
234
# ```
232
235
# JUMPDEST(#)
@@ -238,30 +241,13 @@ def test_worst_returndatacopy(
238
241
# ```
239
242
# The goal is that once per (big) loop iteration, the helper contract is called to
240
243
# 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
244
261
- state_test (
245
+ benchmark_test (
262
246
pre = pre ,
263
247
post = {},
264
- tx = tx ,
248
+ code_generator = JumpLoopGenerator (
249
+ setup = returndata_gen , attack_block = attack_block , cleanup = returndata_gen
250
+ ),
265
251
)
266
252
267
253
@@ -282,42 +268,24 @@ def test_worst_returndatacopy(
282
268
],
283
269
)
284
270
def test_worst_mcopy (
285
- state_test : StateTestFiller ,
271
+ benchmark_test : BenchmarkTestFiller ,
286
272
pre : Alloc ,
287
- fork : Fork ,
288
273
size : int ,
289
274
fixed_src_dst : bool ,
290
- gas_benchmark_value : int ,
291
275
):
292
276
"""Test running a block filled with MCOPY executions."""
293
- max_code_size = fork .max_code_size ()
277
+ src_dst = 0 if fixed_src_dst else Op .MOD (Op .GAS , 7 )
278
+ attack_block = Op .MCOPY (src_dst , src_dst , size )
294
279
295
280
mem_touch = (
296
281
Op .MSTORE8 (0 , Op .GAS ) + Op .MSTORE8 (size // 2 , Op .GAS ) + Op .MSTORE8 (size - 1 , Op .GAS )
297
282
if size > 0
298
283
else Bytecode ()
299
284
)
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 (
285
+ benchmark_test (
320
286
pre = pre ,
321
287
post = {},
322
- tx = tx ,
288
+ code_generator = JumpLoopGenerator (
289
+ setup = mem_touch , attack_block = attack_block , cleanup = mem_touch
290
+ ),
323
291
)
0 commit comments