Skip to content

feat(tests): add benchmark for the worst initcode jumpdest analysis #1646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions tests/zkevm/test_worst_bytecode.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
Bytecode,
Environment,
Hash,
StateTestFiller,
Transaction,
While,
compute_create2_address,
Expand Down Expand Up @@ -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,
)
Loading