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,94 @@ 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
+ pre : Alloc ,
226
+ initcode_size : int ,
227
+ pattern : Bytecode ,
228
+ ):
229
+ """
230
+ Test the jumpdest analysis performance of the initcode.
231
+
232
+ This benchmark places a very long initcode in the memory and then invoke CREATE instructions
233
+ with this initcode up to the block gas limit. The initcode itself has minimal execution time
234
+ but forces the EVM to perform the full jumpdest analysis on the parametrized byte pattern.
235
+ The initicode is modified by mixing-in the returned create address between CREATE invocations
236
+ to prevent caching.
237
+ """
238
+ # Expand the initcode pattern to the transaction data so it can be used in CALLDATACOPY
239
+ # in the main contract. TODO: tune the tx_data_len param.
240
+ tx_data_len = 1024
241
+ tx_data = pattern * (tx_data_len // len (pattern ))
242
+ tx_data += (tx_data_len - len (tx_data )) * bytes (Op .JUMPDEST )
243
+ assert len (tx_data ) == tx_data_len
244
+ assert initcode_size % len (tx_data ) == 0
245
+
246
+ # Prepare the initcode in memory.
247
+ code_prepare_initcode = sum (
248
+ (
249
+ Op .CALLDATACOPY (dest_offset = i * len (tx_data ), offset = 0 , size = Op .CALLDATASIZE )
250
+ for i in range (initcode_size // len (tx_data ))
251
+ ),
252
+ Bytecode (),
253
+ )
254
+
255
+ # At the start of the initcode execution, jump to the last opcode.
256
+ # This forces EVM to do the full jumpdest analysis.
257
+ initcode_prefix = Op .JUMP (initcode_size - 1 )
258
+ code_prepare_initcode += Op .MSTORE (
259
+ 0 , Op .PUSH32 [bytes (initcode_prefix ).ljust (32 , bytes (Op .JUMPDEST ))]
260
+ )
261
+
262
+ # Make sure the last opcode in the initcode is JUMPDEST.
263
+ code_prepare_initcode += Op .MSTORE (initcode_size - 32 , Op .PUSH32 [bytes (Op .JUMPDEST ) * 32 ])
264
+
265
+ code_invoke_create = (
266
+ Op .PUSH1 [len (initcode_prefix )]
267
+ + Op .MSTORE
268
+ + Op .CREATE (value = Op .PUSH0 , offset = Op .PUSH0 , size = Op .MSIZE )
269
+ )
270
+
271
+ initial_random = Op .PUSH0
272
+ code_prefix = code_prepare_initcode + initial_random
273
+ code_loop_header = Op .JUMPDEST
274
+ code_loop_footer = Op .JUMP (len (code_prefix ))
275
+ code_loop_body_len = (
276
+ MAX_BYTECODE_SIZE - len (code_prefix ) - len (code_loop_header ) - len (code_loop_footer )
277
+ )
278
+
279
+ code_loop_body = (code_loop_body_len // len (code_invoke_create )) * bytes (code_invoke_create )
280
+ code = code_prefix + code_loop_header + code_loop_body + code_loop_footer
281
+ assert (MAX_BYTECODE_SIZE - len (code_invoke_create )) < len (code ) <= MAX_BYTECODE_SIZE
282
+
283
+ env = Environment ()
284
+
285
+ tx = Transaction (
286
+ to = pre .deploy_contract (code = code ),
287
+ data = tx_data ,
288
+ gas_limit = env .gas_limit ,
289
+ sender = pre .fund_eoa (),
290
+ )
291
+
292
+ state_test (
293
+ env = env ,
294
+ pre = pre ,
295
+ post = {},
296
+ tx = tx ,
297
+ )
0 commit comments