Skip to content

Commit 5b845e3

Browse files
committed
new(tests): add benchmarks with mass binop opcodes
This adds the benchmarks for EVM execution focused on binary instructions (takes two values, produces one). The benchmark has simple structure: the contract code is the infinite loop will up to the code size limit with the parametrized binop applied to two initial values balanced with DUP2 instruction.
1 parent d848e06 commit 5b845e3

File tree

1 file changed

+167
-1
lines changed

1 file changed

+167
-1
lines changed

tests/zkevm/test_worst_compute.py

+167-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
import pytest
1111

1212
from ethereum_test_forks import Fork
13-
from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction
13+
from ethereum_test_tools import (
14+
Alloc,
15+
Block,
16+
BlockchainTestFiller,
17+
Environment,
18+
Transaction,
19+
)
1420
from ethereum_test_tools.vm.opcode import Opcodes as Op
1521

1622
REFERENCE_SPEC_GIT_PATH = "TODO"
@@ -170,3 +176,163 @@ def test_worst_modexp(
170176
post={},
171177
blocks=[Block(txs=[tx])],
172178
)
179+
180+
181+
DEFAULT_BINOP_ARGS = (
182+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
183+
0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001,
184+
)
185+
186+
187+
@pytest.mark.valid_from("Cancun")
188+
@pytest.mark.parametrize(
189+
"opcode,opcode_args",
190+
[
191+
(
192+
Op.ADD,
193+
DEFAULT_BINOP_ARGS,
194+
),
195+
(
196+
Op.MUL,
197+
DEFAULT_BINOP_ARGS,
198+
),
199+
(
200+
# This has the cycle of 2, after two SUBs values are back to initials.
201+
Op.SUB,
202+
DEFAULT_BINOP_ARGS,
203+
),
204+
(
205+
# This has the cycle of 2:
206+
# v[0] = a // b
207+
# v[1] = a // v[0] = a // (a // b) = b
208+
# v[2] = a // b
209+
Op.DIV,
210+
(
211+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
212+
# We want the first divisor to be slightly bigger than 2**128:
213+
# this is the worst case for the division algorithm.
214+
0x100000000000000000000000000000033,
215+
),
216+
),
217+
(
218+
# Same as DIV, but the numerator made positive, and the divisor made negative.
219+
Op.SDIV,
220+
(
221+
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
222+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD,
223+
),
224+
),
225+
(
226+
# This scenario is not suitable for MOD because the values quickly become 0.
227+
Op.MOD,
228+
DEFAULT_BINOP_ARGS,
229+
),
230+
(
231+
# This scenario is not suitable for SMOD because the values quickly become 0.
232+
Op.SMOD,
233+
DEFAULT_BINOP_ARGS,
234+
),
235+
(
236+
# This keeps the values unchanged, pow(2**256-1, 2**256-1, 2**256) == 2**256-1.
237+
Op.EXP,
238+
(
239+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
240+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
241+
),
242+
),
243+
(
244+
# Not great because we always sign-extend the 4 bytes.
245+
Op.SIGNEXTEND,
246+
(
247+
3,
248+
0xFFDADADA, # Negative to have more work.
249+
),
250+
),
251+
(
252+
Op.LT, # Keeps getting result 1.
253+
(0, 1),
254+
),
255+
(
256+
Op.GT, # Keeps getting result 0.
257+
(0, 1),
258+
),
259+
(
260+
Op.SLT, # Keeps getting result 1.
261+
(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 1),
262+
),
263+
(
264+
Op.SGT, # Keeps getting result 0.
265+
(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 1),
266+
),
267+
(
268+
# The worst case is if the arguments are equal (no early return),
269+
# so let's keep it comparing ones.
270+
Op.EQ,
271+
(1, 1),
272+
),
273+
(
274+
Op.AND,
275+
DEFAULT_BINOP_ARGS,
276+
),
277+
(
278+
Op.OR,
279+
DEFAULT_BINOP_ARGS,
280+
),
281+
(
282+
Op.XOR,
283+
DEFAULT_BINOP_ARGS,
284+
),
285+
(
286+
Op.BYTE, # Keep extracting the last byte: 0x2F.
287+
(31, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
288+
),
289+
(
290+
Op.SHL, # Shift by 1 until getting 0.
291+
(1, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
292+
),
293+
(
294+
Op.SHR, # Shift by 1 until getting 0.
295+
(1, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
296+
),
297+
(
298+
Op.SAR, # Shift by 1 until getting -1.
299+
(1, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
300+
),
301+
],
302+
ids=lambda param: "" if isinstance(param, tuple) else param,
303+
)
304+
def test_worst_binop_simple(
305+
blockchain_test: BlockchainTestFiller,
306+
pre: Alloc,
307+
opcode: Op,
308+
opcode_args: tuple[int, int],
309+
):
310+
"""
311+
Test running a block with as many binary instructions (takes two args, produces one value)
312+
as possible. The execution starts with two initial values on the stack, and the stack is
313+
balanced by the DUP2 instruction.
314+
"""
315+
env = Environment()
316+
317+
tx_data = b"".join(arg.to_bytes(32, byteorder="big") for arg in opcode_args)
318+
319+
code_prefix = Op.JUMPDEST + Op.CALLDATALOAD(0) + Op.CALLDATALOAD(32)
320+
code_suffix = Op.POP + Op.POP + Op.PUSH0 + Op.JUMP
321+
code_body_len = MAX_CODE_SIZE - len(code_prefix) - len(code_suffix)
322+
code_body = (Op.DUP2 + opcode) * (code_body_len // 2)
323+
code = code_prefix + code_body + code_suffix
324+
assert len(code) == MAX_CODE_SIZE - 1
325+
326+
tx = Transaction(
327+
to=pre.deploy_contract(code=code),
328+
data=tx_data,
329+
gas_limit=env.gas_limit,
330+
sender=pre.fund_eoa(),
331+
)
332+
333+
blockchain_test(
334+
env=env,
335+
pre=pre,
336+
post={},
337+
blocks=[Block(txs=[tx])],
338+
)

0 commit comments

Comments
 (0)