Skip to content

Commit 2303aa9

Browse files
committed
feat(tests): add benchmark for the worst initcode jumpdest analysis
1 parent e5e3ebe commit 2303aa9

File tree

1 file changed

+97
-6
lines changed

1 file changed

+97
-6
lines changed

tests/zkevm/test_worst_bytecode.py

Lines changed: 97 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,94 @@ 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+
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

Comments
 (0)