18
18
Bytecode ,
19
19
Environment ,
20
20
Hash ,
21
+ StateTestFiller ,
21
22
Transaction ,
22
23
While ,
23
24
compute_create2_address ,
24
25
)
25
26
from ethereum_test_tools .vm .opcode import Opcodes as Op
27
+ from pytest_plugins .execute .pre_alloc import MAX_BYTECODE_SIZE , MAX_INITCODE_SIZE
26
28
27
29
REFERENCE_SPEC_GIT_PATH = "TODO"
28
30
REFERENCE_SPEC_VERSION = "TODO"
29
31
30
- MAX_CONTRACT_SIZE = 24 * 1024 # TODO: This could be a fork property
31
-
32
32
XOR_TABLE_SIZE = 256
33
33
XOR_TABLE = [Hash (i ).sha256 () for i in range (XOR_TABLE_SIZE )]
34
34
@@ -86,13 +86,13 @@ def test_worst_bytecode_single_opcode(
86
86
)
87
87
+ Op .POP
88
88
),
89
- condition = Op .LT (Op .MSIZE , MAX_CONTRACT_SIZE ),
89
+ condition = Op .LT (Op .MSIZE , MAX_BYTECODE_SIZE ),
90
90
)
91
91
# Despite the whole contract has random bytecode, we make the first opcode be a STOP
92
92
# so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
93
93
# intended.
94
94
+ Op .MSTORE8 (0 , 0x00 )
95
- + Op .RETURN (0 , MAX_CONTRACT_SIZE )
95
+ + Op .RETURN (0 , MAX_BYTECODE_SIZE )
96
96
)
97
97
initcode_address = pre .deploy_contract (code = initcode )
98
98
@@ -180,11 +180,11 @@ def test_worst_bytecode_single_opcode(
180
180
)
181
181
)
182
182
183
- if len (attack_code ) > MAX_CONTRACT_SIZE :
183
+ if len (attack_code ) > MAX_BYTECODE_SIZE :
184
184
# TODO: A workaround could be to split the opcode code into multiple contracts
185
185
# and call them in sequence.
186
186
raise ValueError (
187
- f"Code size { len (attack_code )} exceeds maximum code size { MAX_CONTRACT_SIZE } "
187
+ f"Code size { len (attack_code )} exceeds maximum code size { MAX_BYTECODE_SIZE } "
188
188
)
189
189
opcode_address = pre .deploy_contract (code = attack_code )
190
190
opcode_tx = Transaction (
@@ -204,3 +204,100 @@ def test_worst_bytecode_single_opcode(
204
204
],
205
205
exclude_full_post_state_in_output = True ,
206
206
)
207
+
208
+
209
+ @pytest .mark .valid_from ("Cancun" )
210
+ @pytest .mark .parametrize ("initcode_size" , [MAX_INITCODE_SIZE ])
211
+ @pytest .mark .parametrize (
212
+ "pattern" ,
213
+ [
214
+ Op .STOP ,
215
+ Op .JUMPDEST ,
216
+ Op .PUSH1 [bytes (Op .JUMPDEST )],
217
+ Op .PUSH2 [bytes (Op .JUMPDEST + Op .JUMPDEST )],
218
+ Op .PUSH1 [bytes (Op .JUMPDEST )] + Op .JUMPDEST ,
219
+ Op .PUSH2 [bytes (Op .JUMPDEST + Op .JUMPDEST )] + Op .JUMPDEST ,
220
+ ],
221
+ ids = lambda x : x .hex (),
222
+ )
223
+ def test_worst_initcode_jumpdest_analysis (
224
+ state_test : StateTestFiller ,
225
+ fork : Fork ,
226
+ pre : Alloc ,
227
+ initcode_size : int ,
228
+ pattern : Bytecode ,
229
+ ):
230
+ """
231
+ Test the jumpdest analysis performance of the initcode.
232
+
233
+ This benchmark places a very long initcode in the memory and then invoke CREATE instructions
234
+ with this initcode up to the block gas limit. The initcode itself has minimal execution time
235
+ but forces the EVM to perform the full jumpdest analysis on the parametrized byte pattern.
236
+ The initicode is modified by mixing-in the returned create address between CREATE invocations
237
+ to prevent caching.
238
+ """
239
+ # Expand the initcode pattern to the transaction data so it can be used in CALLDATACOPY
240
+ # in the main contract. TODO: tune the tx_data_len param.
241
+ tx_data_len = 1024
242
+ tx_data = pattern * (tx_data_len // len (pattern ))
243
+ tx_data += (tx_data_len - len (tx_data )) * bytes (Op .JUMPDEST )
244
+ assert len (tx_data ) == tx_data_len
245
+ assert initcode_size % len (tx_data ) == 0
246
+
247
+ # Prepare the initcode in memory.
248
+ code_prepare_initcode = sum (
249
+ (
250
+ Op .CALLDATACOPY (dest_offset = i * len (tx_data ), offset = 0 , size = Op .CALLDATASIZE )
251
+ for i in range (initcode_size // len (tx_data ))
252
+ ),
253
+ Bytecode (),
254
+ )
255
+
256
+ # At the start of the initcode execution, jump to the last opcode.
257
+ # This forces EVM to do the full jumpdest analysis.
258
+ initcode_prefix = Op .JUMP (initcode_size - 1 )
259
+ code_prepare_initcode += Op .MSTORE (
260
+ 0 , Op .PUSH32 [bytes (initcode_prefix ).ljust (32 , bytes (Op .JUMPDEST ))]
261
+ )
262
+
263
+ # Make sure the last opcode in the initcode is JUMPDEST.
264
+ code_prepare_initcode += Op .MSTORE (initcode_size - 32 , Op .PUSH32 [bytes (Op .JUMPDEST ) * 32 ])
265
+
266
+ # We are using the Paris fork where there is no initcode size limit.
267
+ # This allows us to test the increased initcode limits.
268
+ # However, Paris doesn't have PUSH0 so simulate it with PUSH1.
269
+ push0 = Op .PUSH0 if Op .PUSH0 in fork .valid_opcodes () else Op .PUSH1 [0 ]
270
+
271
+ code_invoke_create = (
272
+ Op .PUSH1 [len (initcode_prefix )]
273
+ + Op .MSTORE
274
+ + Op .CREATE (value = push0 , offset = push0 , size = Op .MSIZE )
275
+ )
276
+
277
+ initial_random = push0
278
+ code_prefix = code_prepare_initcode + initial_random
279
+ code_loop_header = Op .JUMPDEST
280
+ code_loop_footer = Op .JUMP (len (code_prefix ))
281
+ code_loop_body_len = (
282
+ MAX_BYTECODE_SIZE - len (code_prefix ) - len (code_loop_header ) - len (code_loop_footer )
283
+ )
284
+
285
+ code_loop_body = (code_loop_body_len // len (code_invoke_create )) * bytes (code_invoke_create )
286
+ code = code_prefix + code_loop_header + code_loop_body + code_loop_footer
287
+ assert (MAX_BYTECODE_SIZE - len (code_invoke_create )) < len (code ) <= MAX_BYTECODE_SIZE
288
+
289
+ env = Environment ()
290
+
291
+ tx = Transaction (
292
+ to = pre .deploy_contract (code = code ),
293
+ data = tx_data ,
294
+ gas_limit = env .gas_limit ,
295
+ sender = pre .fund_eoa (),
296
+ )
297
+
298
+ state_test (
299
+ env = env ,
300
+ pre = pre ,
301
+ post = {},
302
+ tx = tx ,
303
+ )
0 commit comments