Skip to content

Commit a6d2310

Browse files
authored
feat(tests): add benchmarks with mass binop opcodes (#1591)
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 8435e43 commit a6d2310

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed

tests/zkevm/test_worst_compute.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,163 @@ def test_worst_jumpdests(
359359
post={},
360360
blocks=[Block(txs=txs)],
361361
)
362+
363+
364+
DEFAULT_BINOP_ARGS = (
365+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
366+
0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001,
367+
)
368+
369+
370+
@pytest.mark.valid_from("Cancun")
371+
@pytest.mark.parametrize(
372+
"opcode,opcode_args",
373+
[
374+
(
375+
Op.ADD,
376+
DEFAULT_BINOP_ARGS,
377+
),
378+
(
379+
Op.MUL,
380+
DEFAULT_BINOP_ARGS,
381+
),
382+
(
383+
# This has the cycle of 2, after two SUBs values are back to initials.
384+
Op.SUB,
385+
DEFAULT_BINOP_ARGS,
386+
),
387+
(
388+
# This has the cycle of 2:
389+
# v[0] = a // b
390+
# v[1] = a // v[0] = a // (a // b) = b
391+
# v[2] = a // b
392+
Op.DIV,
393+
(
394+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
395+
# We want the first divisor to be slightly bigger than 2**128:
396+
# this is the worst case for the division algorithm.
397+
0x100000000000000000000000000000033,
398+
),
399+
),
400+
(
401+
# Same as DIV, but the numerator made positive, and the divisor made negative.
402+
Op.SDIV,
403+
(
404+
0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
405+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD,
406+
),
407+
),
408+
(
409+
# This scenario is not suitable for MOD because the values quickly become 0.
410+
Op.MOD,
411+
DEFAULT_BINOP_ARGS,
412+
),
413+
(
414+
# This scenario is not suitable for SMOD because the values quickly become 0.
415+
Op.SMOD,
416+
DEFAULT_BINOP_ARGS,
417+
),
418+
(
419+
# This keeps the values unchanged, pow(2**256-1, 2**256-1, 2**256) == 2**256-1.
420+
Op.EXP,
421+
(
422+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
423+
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,
424+
),
425+
),
426+
(
427+
# Not great because we always sign-extend the 4 bytes.
428+
Op.SIGNEXTEND,
429+
(
430+
3,
431+
0xFFDADADA, # Negative to have more work.
432+
),
433+
),
434+
(
435+
Op.LT, # Keeps getting result 1.
436+
(0, 1),
437+
),
438+
(
439+
Op.GT, # Keeps getting result 0.
440+
(0, 1),
441+
),
442+
(
443+
Op.SLT, # Keeps getting result 1.
444+
(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 1),
445+
),
446+
(
447+
Op.SGT, # Keeps getting result 0.
448+
(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 1),
449+
),
450+
(
451+
# The worst case is if the arguments are equal (no early return),
452+
# so let's keep it comparing ones.
453+
Op.EQ,
454+
(1, 1),
455+
),
456+
(
457+
Op.AND,
458+
DEFAULT_BINOP_ARGS,
459+
),
460+
(
461+
Op.OR,
462+
DEFAULT_BINOP_ARGS,
463+
),
464+
(
465+
Op.XOR,
466+
DEFAULT_BINOP_ARGS,
467+
),
468+
(
469+
Op.BYTE, # Keep extracting the last byte: 0x2F.
470+
(31, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
471+
),
472+
(
473+
Op.SHL, # Shift by 1 until getting 0.
474+
(1, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
475+
),
476+
(
477+
Op.SHR, # Shift by 1 until getting 0.
478+
(1, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
479+
),
480+
(
481+
Op.SAR, # Shift by 1 until getting -1.
482+
(1, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F),
483+
),
484+
],
485+
ids=lambda param: "" if isinstance(param, tuple) else param,
486+
)
487+
def test_worst_binop_simple(
488+
blockchain_test: BlockchainTestFiller,
489+
pre: Alloc,
490+
opcode: Op,
491+
opcode_args: tuple[int, int],
492+
):
493+
"""
494+
Test running a block with as many binary instructions (takes two args, produces one value)
495+
as possible. The execution starts with two initial values on the stack, and the stack is
496+
balanced by the DUP2 instruction.
497+
"""
498+
env = Environment()
499+
500+
tx_data = b"".join(arg.to_bytes(32, byteorder="big") for arg in opcode_args)
501+
502+
code_prefix = Op.JUMPDEST + Op.CALLDATALOAD(0) + Op.CALLDATALOAD(32)
503+
code_suffix = Op.POP + Op.POP + Op.PUSH0 + Op.JUMP
504+
code_body_len = MAX_CODE_SIZE - len(code_prefix) - len(code_suffix)
505+
code_body = (Op.DUP2 + opcode) * (code_body_len // 2)
506+
code = code_prefix + code_body + code_suffix
507+
assert len(code) == MAX_CODE_SIZE - 1
508+
509+
tx = Transaction(
510+
to=pre.deploy_contract(code=code),
511+
data=tx_data,
512+
gas_limit=env.gas_limit,
513+
sender=pre.fund_eoa(),
514+
)
515+
516+
blockchain_test(
517+
env=env,
518+
pre=pre,
519+
post={},
520+
blocks=[Block(txs=[tx])],
521+
)

0 commit comments

Comments
 (0)