From e4eba6bb17175d89ce4082f583ebb4528c122941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Fri, 23 May 2025 16:19:55 +0200 Subject: [PATCH] feat(tests): add benchmark for the worst initcode jumpdest analysis --- tests/zkevm/test_worst_bytecode.py | 94 ++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/tests/zkevm/test_worst_bytecode.py b/tests/zkevm/test_worst_bytecode.py index 4343da0a0e4..ae0d2364939 100644 --- a/tests/zkevm/test_worst_bytecode.py +++ b/tests/zkevm/test_worst_bytecode.py @@ -18,6 +18,7 @@ Bytecode, Environment, Hash, + StateTestFiller, Transaction, While, compute_create2_address, @@ -221,3 +222,96 @@ def test_worst_bytecode_single_opcode( ], exclude_full_post_state_in_output=True, ) + + +@pytest.mark.valid_from("Cancun") +@pytest.mark.parametrize( + "pattern", + [ + Op.STOP, + Op.JUMPDEST, + Op.PUSH1[bytes(Op.JUMPDEST)], + Op.PUSH2[bytes(Op.JUMPDEST + Op.JUMPDEST)], + Op.PUSH1[bytes(Op.JUMPDEST)] + Op.JUMPDEST, + Op.PUSH2[bytes(Op.JUMPDEST + Op.JUMPDEST)] + Op.JUMPDEST, + ], + ids=lambda x: x.hex(), +) +def test_worst_initcode_jumpdest_analysis( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + pattern: Bytecode, +): + """ + Test the jumpdest analysis performance of the initcode. + + This benchmark places a very long initcode in the memory and then invoke CREATE instructions + with this initcode up to the block gas limit. The initcode itself has minimal execution time + but forces the EVM to perform the full jumpdest analysis on the parametrized byte pattern. + The initicode is modified by mixing-in the returned create address between CREATE invocations + to prevent caching. + """ + max_code_size = fork.max_code_size() + initcode_size = fork.max_initcode_size() + + # Expand the initcode pattern to the transaction data so it can be used in CALLDATACOPY + # in the main contract. TODO: tune the tx_data_len param. + tx_data_len = 1024 + tx_data = pattern * (tx_data_len // len(pattern)) + tx_data += (tx_data_len - len(tx_data)) * bytes(Op.JUMPDEST) + assert len(tx_data) == tx_data_len + assert initcode_size % len(tx_data) == 0 + + # Prepare the initcode in memory. + code_prepare_initcode = sum( + ( + Op.CALLDATACOPY(dest_offset=i * len(tx_data), offset=0, size=Op.CALLDATASIZE) + for i in range(initcode_size // len(tx_data)) + ), + Bytecode(), + ) + + # At the start of the initcode execution, jump to the last opcode. + # This forces EVM to do the full jumpdest analysis. + initcode_prefix = Op.JUMP(initcode_size - 1) + code_prepare_initcode += Op.MSTORE( + 0, Op.PUSH32[bytes(initcode_prefix).ljust(32, bytes(Op.JUMPDEST))] + ) + + # Make sure the last opcode in the initcode is JUMPDEST. + code_prepare_initcode += Op.MSTORE(initcode_size - 32, Op.PUSH32[bytes(Op.JUMPDEST) * 32]) + + code_invoke_create = ( + Op.PUSH1[len(initcode_prefix)] + + Op.MSTORE + + Op.CREATE(value=Op.PUSH0, offset=Op.PUSH0, size=Op.MSIZE) + ) + + initial_random = Op.PUSH0 + code_prefix = code_prepare_initcode + initial_random + code_loop_header = Op.JUMPDEST + code_loop_footer = Op.JUMP(len(code_prefix)) + code_loop_body_len = ( + max_code_size - len(code_prefix) - len(code_loop_header) - len(code_loop_footer) + ) + + code_loop_body = (code_loop_body_len // len(code_invoke_create)) * bytes(code_invoke_create) + code = code_prefix + code_loop_header + code_loop_body + code_loop_footer + assert (max_code_size - len(code_invoke_create)) < len(code) <= max_code_size + + env = Environment() + + tx = Transaction( + to=pre.deploy_contract(code=code), + data=tx_data, + gas_limit=env.gas_limit, + sender=pre.fund_eoa(), + ) + + state_test( + env=env, + pre=pre, + post={}, + tx=tx, + )