10
10
import pytest
11
11
12
12
from ethereum_test_forks import Fork
13
- from ethereum_test_tools import Alloc , Block , BlockchainTestFiller , Environment , Transaction
13
+ from ethereum_test_tools import (
14
+ Address ,
15
+ Alloc ,
16
+ Block ,
17
+ BlockchainTestFiller ,
18
+ Bytecode ,
19
+ Environment ,
20
+ Transaction ,
21
+ )
14
22
from ethereum_test_tools .vm .opcode import Opcodes as Op
15
23
16
24
REFERENCE_SPEC_GIT_PATH = "TODO"
17
25
REFERENCE_SPEC_VERSION = "TODO"
18
26
19
27
MAX_CODE_SIZE = 24 * 1024
20
28
KECCAK_RATE = 136
29
+ ECRECOVER_GAS_COST = 3_000
21
30
22
31
23
32
@pytest .mark .valid_from ("Cancun" )
24
- @pytest .mark .parametrize (
25
- "gas_limit" ,
26
- [
27
- 36_000_000 ,
28
- ],
29
- )
30
33
def test_worst_keccak (
31
34
blockchain_test : BlockchainTestFiller ,
32
35
pre : Alloc ,
33
36
fork : Fork ,
34
- gas_limit : int ,
35
37
):
36
38
"""Test running a block with as many KECCAK256 permutations as possible."""
37
- env = Environment (gas_limit = gas_limit )
39
+ env = Environment ()
38
40
39
41
# Intrinsic gas cost is paid once.
40
42
intrinsic_gas_calculator = fork .transaction_intrinsic_cost_calculator ()
41
- available_gas = gas_limit - intrinsic_gas_calculator ()
43
+ available_gas = env . gas_limit - intrinsic_gas_calculator ()
42
44
43
45
gsc = fork .gas_costs ()
44
46
mem_exp_gas_calculator = fork .memory_expansion_gas_calculator ()
@@ -90,11 +92,8 @@ def test_worst_keccak(
90
92
91
93
tx = Transaction (
92
94
to = code_address ,
93
- gas_limit = gas_limit ,
94
- gas_price = 10 ,
95
+ gas_limit = env .gas_limit ,
95
96
sender = pre .fund_eoa (),
96
- data = [],
97
- value = 0 ,
98
97
)
99
98
100
99
blockchain_test (
@@ -105,22 +104,94 @@ def test_worst_keccak(
105
104
)
106
105
107
106
108
- @pytest .mark .zkevm
109
107
@pytest .mark .valid_from ("Cancun" )
110
108
@pytest .mark .parametrize (
111
- "gas_limit " ,
109
+ "address,static_cost,per_word_dynamic_cost,bytes_per_unit_of_work " ,
112
110
[
113
- Environment ().gas_limit ,
111
+ pytest .param (0x02 , 60 , 12 , 64 , id = "SHA2-256" ),
112
+ pytest .param (0x03 , 600 , 120 , 64 , id = "RIPEMD-160" ),
113
+ pytest .param (0x04 , 15 , 3 , 1 , id = "IDENTITY" ),
114
114
],
115
115
)
116
+ def test_worst_precompile_only_data_input (
117
+ blockchain_test : BlockchainTestFiller ,
118
+ pre : Alloc ,
119
+ fork : Fork ,
120
+ address : Address ,
121
+ static_cost : int ,
122
+ per_word_dynamic_cost : int ,
123
+ bytes_per_unit_of_work : int ,
124
+ ):
125
+ """Test running a block with as many precompile calls which have a single `data` input."""
126
+ env = Environment ()
127
+
128
+ # Intrinsic gas cost is paid once.
129
+ intrinsic_gas_calculator = fork .transaction_intrinsic_cost_calculator ()
130
+ available_gas = env .gas_limit - intrinsic_gas_calculator ()
131
+
132
+ gsc = fork .gas_costs ()
133
+ mem_exp_gas_calculator = fork .memory_expansion_gas_calculator ()
134
+
135
+ # Discover the optimal input size to maximize precompile work, not precompile calls.
136
+ max_work = 0
137
+ optimal_input_length = 0
138
+ for input_length in range (1 , 1_000_000 , 32 ):
139
+ parameters_gas = (
140
+ gsc .G_BASE # PUSH0 = arg offset
141
+ + gsc .G_BASE # PUSH0 = arg size
142
+ + gsc .G_BASE # PUSH0 = arg size
143
+ + gsc .G_VERY_LOW # PUSH0 = arg offset
144
+ + gsc .G_VERY_LOW # PUSHN = address
145
+ + gsc .G_BASE # GAS
146
+ )
147
+ iteration_gas_cost = (
148
+ parameters_gas
149
+ + + static_cost # Precompile static cost
150
+ + math .ceil (input_length / 32 ) * per_word_dynamic_cost # Precompile dynamic cost
151
+ + gsc .G_BASE # POP
152
+ )
153
+ # From the available gas, we substract the mem expansion costs considering we know the
154
+ # current input size length.
155
+ available_gas_after_expansion = max (
156
+ 0 , available_gas - mem_exp_gas_calculator (new_bytes = input_length )
157
+ )
158
+ # Calculate how many calls we can do.
159
+ num_calls = available_gas_after_expansion // iteration_gas_cost
160
+ total_work = num_calls * math .ceil (input_length / bytes_per_unit_of_work )
161
+
162
+ # If we found an input size that is better (reg permutations/gas), then save it.
163
+ if total_work > max_work :
164
+ max_work = total_work
165
+ optimal_input_length = input_length
166
+
167
+ calldata = Op .CODECOPY (0 , 0 , optimal_input_length )
168
+ attack_block = Op .POP (Op .STATICCALL (Op .GAS , address , 0 , optimal_input_length , 0 , 0 ))
169
+ code = code_loop_precompile_call (calldata , attack_block )
170
+
171
+ code_address = pre .deploy_contract (code = code )
172
+
173
+ tx = Transaction (
174
+ to = code_address ,
175
+ gas_limit = env .gas_limit ,
176
+ sender = pre .fund_eoa (),
177
+ )
178
+
179
+ blockchain_test (
180
+ env = env ,
181
+ pre = pre ,
182
+ post = {},
183
+ blocks = [Block (txs = [tx ])],
184
+ )
185
+
186
+
187
+ @pytest .mark .valid_from ("Cancun" )
116
188
def test_worst_modexp (
117
189
blockchain_test : BlockchainTestFiller ,
118
190
pre : Alloc ,
119
191
fork : Fork ,
120
- gas_limit : int ,
121
192
):
122
193
"""Test running a block with as many MODEXP calls as possible."""
123
- env = Environment (gas_limit = gas_limit )
194
+ env = Environment ()
124
195
125
196
base_mod_length = 32
126
197
exp_length = 32
@@ -144,23 +215,48 @@ def test_worst_modexp(
144
215
iter_complexity = exp .bit_length () - 1
145
216
gas_cost = math .floor ((mul_complexity * iter_complexity ) / 3 )
146
217
attack_block = Op .POP (Op .STATICCALL (gas_cost , 0x5 , 0 , 32 * 6 , 0 , 0 ))
218
+ code = code_loop_precompile_call (calldata , attack_block )
147
219
148
- # The attack contract is: JUMPDEST + [attack_block]* + PUSH0 + JUMP
149
- jumpdest = Op .JUMPDEST
150
- jump_back = Op .JUMP (len (calldata ))
151
- max_iters_loop = (MAX_CODE_SIZE - len (calldata ) - len (jumpdest ) - len (jump_back )) // len (
152
- attack_block
220
+ code_address = pre .deploy_contract (code = code )
221
+
222
+ tx = Transaction (
223
+ to = code_address ,
224
+ gas_limit = env .gas_limit ,
225
+ sender = pre .fund_eoa (),
153
226
)
154
- code = calldata + jumpdest + sum ([attack_block ] * max_iters_loop ) + jump_back
155
- if len (code ) > MAX_CODE_SIZE :
156
- # Must never happen, but keep it as a sanity check.
157
- raise ValueError (f"Code size { len (code )} exceeds maximum code size { MAX_CODE_SIZE } " )
158
227
159
- code_address = pre .deploy_contract (code = code )
228
+ blockchain_test (
229
+ env = env ,
230
+ pre = pre ,
231
+ post = {},
232
+ blocks = [Block (txs = [tx ])],
233
+ )
234
+
235
+
236
+ @pytest .mark .valid_from ("Cancun" )
237
+ def test_worst_ecrecover (
238
+ blockchain_test : BlockchainTestFiller ,
239
+ pre : Alloc ,
240
+ fork : Fork ,
241
+ ):
242
+ """Test running a block with as many ECRECOVER calls as possible."""
243
+ env = Environment ()
244
+
245
+ # Calldata
246
+ calldata = (
247
+ Op .MSTORE (0 * 32 , 0x38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E )
248
+ + Op .MSTORE (1 * 32 , 27 )
249
+ + Op .MSTORE (2 * 32 , 0x38D18ACB67D25C8BB9942764B62F18E17054F66A817BD4295423ADF9ED98873E )
250
+ + Op .MSTORE (3 * 32 , 0x789D1DD423D25F0772D2748D60F7E4B81BB14D086EBA8E8E8EFB6DCFF8A4AE02 )
251
+ )
252
+
253
+ attack_block = Op .POP (Op .STATICCALL (ECRECOVER_GAS_COST , 0x1 , 0 , 32 * 4 , 0 , 0 ))
254
+ code = code_loop_precompile_call (calldata , attack_block )
255
+ code_address = pre .deploy_contract (code = bytes (code ))
160
256
161
257
tx = Transaction (
162
258
to = code_address ,
163
- gas_limit = gas_limit ,
259
+ gas_limit = env . gas_limit ,
164
260
sender = pre .fund_eoa (),
165
261
)
166
262
@@ -170,3 +266,19 @@ def test_worst_modexp(
170
266
post = {},
171
267
blocks = [Block (txs = [tx ])],
172
268
)
269
+
270
+
271
+ def code_loop_precompile_call (calldata : Bytecode , attack_block : Bytecode ):
272
+ """Create a code loop that calls a precompile with the given calldata."""
273
+ # The attack contract is: CALLDATA_PREP + #JUMPDEST + [attack_block]* + JUMP(#)
274
+ jumpdest = Op .JUMPDEST
275
+ jump_back = Op .JUMP (len (calldata ))
276
+ max_iters_loop = (MAX_CODE_SIZE - len (calldata ) - len (jumpdest ) - len (jump_back )) // len (
277
+ attack_block
278
+ )
279
+ code = calldata + jumpdest + sum ([attack_block ] * max_iters_loop ) + jump_back
280
+ if len (code ) > MAX_CODE_SIZE :
281
+ # Must never happen, but keep it as a sanity check.
282
+ raise ValueError (f"Code size { len (code )} exceeds maximum code size { MAX_CODE_SIZE } " )
283
+
284
+ return code
0 commit comments