Skip to content

Commit 5f1d2b2

Browse files
committed
feat(tests): add benchmark for the worst initcode jumpdest analysis
1 parent bceb08f commit 5f1d2b2

File tree

1 file changed

+103
-6
lines changed

1 file changed

+103
-6
lines changed

tests/zkevm/test_worst_bytecode.py

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,17 @@
1818
Bytecode,
1919
Environment,
2020
Hash,
21+
StateTestFiller,
2122
Transaction,
2223
While,
2324
compute_create2_address,
2425
)
2526
from ethereum_test_tools.vm.opcode import Opcodes as Op
27+
from pytest_plugins.execute.pre_alloc import MAX_BYTECODE_SIZE, MAX_INITCODE_SIZE
2628

2729
REFERENCE_SPEC_GIT_PATH = "TODO"
2830
REFERENCE_SPEC_VERSION = "TODO"
2931

30-
MAX_CONTRACT_SIZE = 24 * 1024 # TODO: This could be a fork property
31-
3232
XOR_TABLE_SIZE = 256
3333
XOR_TABLE = [Hash(i).sha256() for i in range(XOR_TABLE_SIZE)]
3434

@@ -86,13 +86,13 @@ def test_worst_bytecode_single_opcode(
8686
)
8787
+ Op.POP
8888
),
89-
condition=Op.LT(Op.MSIZE, MAX_CONTRACT_SIZE),
89+
condition=Op.LT(Op.MSIZE, MAX_BYTECODE_SIZE),
9090
)
9191
# Despite the whole contract has random bytecode, we make the first opcode be a STOP
9292
# so CALL-like attacks return as soon as possible, while EXTCODE(HASH|SIZE) work as
9393
# intended.
9494
+ Op.MSTORE8(0, 0x00)
95-
+ Op.RETURN(0, MAX_CONTRACT_SIZE)
95+
+ Op.RETURN(0, MAX_BYTECODE_SIZE)
9696
)
9797
initcode_address = pre.deploy_contract(code=initcode)
9898

@@ -180,11 +180,11 @@ def test_worst_bytecode_single_opcode(
180180
)
181181
)
182182

183-
if len(attack_code) > MAX_CONTRACT_SIZE:
183+
if len(attack_code) > MAX_BYTECODE_SIZE:
184184
# TODO: A workaround could be to split the opcode code into multiple contracts
185185
# and call them in sequence.
186186
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}"
188188
)
189189
opcode_address = pre.deploy_contract(code=attack_code)
190190
opcode_tx = Transaction(
@@ -204,3 +204,100 @@ def test_worst_bytecode_single_opcode(
204204
],
205205
exclude_full_post_state_in_output=True,
206206
)
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

Comments
 (0)