|
| 1 | +"""Benchmark mixed operations.""" |
| 2 | + |
| 3 | +import pytest |
| 4 | +from ethereum_test_forks import Fork |
| 5 | +from ethereum_test_tools import ( |
| 6 | + Account, |
| 7 | + Address, |
| 8 | + BenchmarkTestFiller, |
| 9 | + Bytecode, |
| 10 | + JumpLoopGenerator, |
| 11 | +) |
| 12 | +from ethereum_test_types import Alloc |
| 13 | +from ethereum_test_vm import Opcodes as Op |
| 14 | + |
| 15 | + |
| 16 | +@pytest.mark.parametrize( |
| 17 | + "pattern", |
| 18 | + [ |
| 19 | + Op.STOP, |
| 20 | + Op.JUMPDEST, |
| 21 | + Op.PUSH1[bytes(Op.JUMPDEST)], |
| 22 | + Op.PUSH2[bytes(Op.JUMPDEST + Op.JUMPDEST)], |
| 23 | + Op.PUSH1[bytes(Op.JUMPDEST)] + Op.JUMPDEST, |
| 24 | + Op.PUSH2[bytes(Op.JUMPDEST + Op.JUMPDEST)] + Op.JUMPDEST, |
| 25 | + ], |
| 26 | + ids=lambda x: x.hex(), |
| 27 | +) |
| 28 | +def test_jumpdest_analysis( |
| 29 | + benchmark_test: BenchmarkTestFiller, |
| 30 | + fork: Fork, |
| 31 | + pattern: Bytecode, |
| 32 | +) -> None: |
| 33 | + """ |
| 34 | + Test the jumpdest analysis performance of the initcode. |
| 35 | +
|
| 36 | + This benchmark places a very long initcode in the memory and then invoke |
| 37 | + CREATE instructions with this initcode up to the block gas limit. The |
| 38 | + initcode itself has minimal execution time but forces the EVM to perform |
| 39 | + the full jumpdest analysis on the parametrized byte pattern. The initicode |
| 40 | + is modified by mixing-in the returned create address between CREATE |
| 41 | + invocations to prevent caching. |
| 42 | + """ |
| 43 | + initcode_size = fork.max_initcode_size() |
| 44 | + |
| 45 | + # Expand the initcode pattern to the transaction data so it can be used in |
| 46 | + # CALLDATACOPY in the main contract. TODO: tune the tx_data_len param. |
| 47 | + tx_data_len = 1024 |
| 48 | + tx_data = pattern * (tx_data_len // len(pattern)) |
| 49 | + tx_data += (tx_data_len - len(tx_data)) * bytes(Op.JUMPDEST) |
| 50 | + assert len(tx_data) == tx_data_len |
| 51 | + assert initcode_size % len(tx_data) == 0 |
| 52 | + |
| 53 | + # Prepare the initcode in memory. |
| 54 | + code_prepare_initcode = sum( |
| 55 | + ( |
| 56 | + Op.CALLDATACOPY( |
| 57 | + dest_offset=i * len(tx_data), offset=0, size=Op.CALLDATASIZE |
| 58 | + ) |
| 59 | + for i in range(initcode_size // len(tx_data)) |
| 60 | + ), |
| 61 | + Bytecode(), |
| 62 | + ) |
| 63 | + |
| 64 | + # At the start of the initcode execution, jump to the last opcode. |
| 65 | + # This forces EVM to do the full jumpdest analysis. |
| 66 | + initcode_prefix = Op.JUMP(initcode_size - 1) |
| 67 | + code_prepare_initcode += Op.MSTORE( |
| 68 | + 0, Op.PUSH32[bytes(initcode_prefix).ljust(32, bytes(Op.JUMPDEST))] |
| 69 | + ) |
| 70 | + |
| 71 | + # Make sure the last opcode in the initcode is JUMPDEST. |
| 72 | + code_prepare_initcode += Op.MSTORE( |
| 73 | + initcode_size - 32, Op.PUSH32[bytes(Op.JUMPDEST) * 32] |
| 74 | + ) |
| 75 | + |
| 76 | + attack_block = ( |
| 77 | + Op.PUSH1[len(initcode_prefix)] |
| 78 | + + Op.MSTORE |
| 79 | + + Op.CREATE(value=Op.PUSH0, offset=Op.PUSH0, size=Op.MSIZE) |
| 80 | + ) |
| 81 | + |
| 82 | + setup = code_prepare_initcode + Op.PUSH0 |
| 83 | + |
| 84 | + benchmark_test( |
| 85 | + code_generator=JumpLoopGenerator( |
| 86 | + setup=setup, |
| 87 | + attack_block=attack_block, |
| 88 | + tx_kwargs={"data": tx_data}, |
| 89 | + ), |
| 90 | + ) |
| 91 | + |
| 92 | + |
| 93 | +@pytest.mark.parametrize( |
| 94 | + "opcode", |
| 95 | + [ |
| 96 | + Op.BALANCE, |
| 97 | + Op.EXTCODESIZE, |
| 98 | + Op.EXTCODEHASH, |
| 99 | + Op.CALL, |
| 100 | + Op.CALLCODE, |
| 101 | + Op.DELEGATECALL, |
| 102 | + Op.STATICCALL, |
| 103 | + ], |
| 104 | +) |
| 105 | +@pytest.mark.parametrize( |
| 106 | + "absent_target", |
| 107 | + [ |
| 108 | + True, |
| 109 | + False, |
| 110 | + ], |
| 111 | +) |
| 112 | +def test_worst_address_state_warm( |
| 113 | + benchmark_test: BenchmarkTestFiller, |
| 114 | + pre: Alloc, |
| 115 | + opcode: Op, |
| 116 | + absent_target: bool, |
| 117 | +) -> None: |
| 118 | + """ |
| 119 | + Test running a block with as many stateful opcodes doing warm access |
| 120 | + for an account. |
| 121 | + """ |
| 122 | + # Setup |
| 123 | + target_addr = Address(100_000) |
| 124 | + post = {} |
| 125 | + if not absent_target: |
| 126 | + code = Op.STOP + Op.JUMPDEST * 100 |
| 127 | + target_addr = pre.deploy_contract(balance=100, code=code) |
| 128 | + post[target_addr] = Account(balance=100, code=code) |
| 129 | + |
| 130 | + # Execution |
| 131 | + setup = Op.MSTORE(0, target_addr) |
| 132 | + attack_block = Op.POP(opcode(address=Op.MLOAD(0))) |
| 133 | + benchmark_test( |
| 134 | + post=post, |
| 135 | + code_generator=JumpLoopGenerator( |
| 136 | + setup=setup, attack_block=attack_block |
| 137 | + ), |
| 138 | + ) |
0 commit comments