|
| 1 | +""" |
| 2 | +abstract: Tests zkEVMs worst-case compute scenarios. |
| 3 | + Tests zkEVMs worst-case compute scenarios. |
| 4 | +
|
| 5 | +Tests running worst-case compute opcodes and precompile scenarios for zkEVMs. |
| 6 | +""" |
| 7 | + |
| 8 | +import math |
| 9 | + |
| 10 | +import pytest |
| 11 | + |
| 12 | +from ethereum_test_forks import Fork |
| 13 | +from ethereum_test_tools import Alloc, Block, BlockchainTestFiller, Environment, Transaction |
| 14 | +from ethereum_test_tools.vm.opcode import Opcodes as Op |
| 15 | + |
| 16 | +REFERENCE_SPEC_GIT_PATH = "TODO" |
| 17 | +REFERENCE_SPEC_VERSION = "TODO" |
| 18 | + |
| 19 | +MAX_CODE_SIZE = 24 * 1024 |
| 20 | +KECCAK_RATE = 136 |
| 21 | + |
| 22 | + |
| 23 | +@pytest.mark.zkevm |
| 24 | +@pytest.mark.valid_from("Cancun") |
| 25 | +@pytest.mark.parametrize( |
| 26 | + "gas_limit", |
| 27 | + [ |
| 28 | + 36_000_000, |
| 29 | + ], |
| 30 | +) |
| 31 | +def test_worst_keccak( |
| 32 | + blockchain_test: BlockchainTestFiller, |
| 33 | + pre: Alloc, |
| 34 | + fork: Fork, |
| 35 | + gas_limit: int, |
| 36 | +): |
| 37 | + """Test running a block with as many KECCAK256 permutations as possible.""" |
| 38 | + env = Environment(gas_limit=gas_limit) |
| 39 | + |
| 40 | + # Intrinsic gas cost is paid once. |
| 41 | + intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() |
| 42 | + available_gas = gas_limit - intrinsic_gas_calculator() |
| 43 | + |
| 44 | + gsc = fork.gas_costs() |
| 45 | + mem_exp_gas_calculator = fork.memory_expansion_gas_calculator() |
| 46 | + |
| 47 | + # Discover the optimal input size to maximize keccak-permutations, not keccak calls. |
| 48 | + # The complication of the discovery arises from the non-linear gas cost of memory expansion. |
| 49 | + max_keccak_perm_per_block = 0 |
| 50 | + optimal_input_length = 0 |
| 51 | + for i in range(1, 1_000_000, 32): |
| 52 | + iteration_gas_cost = ( |
| 53 | + 2 * gsc.G_VERY_LOW # PUSHN + PUSH1 |
| 54 | + + gsc.G_KECCAK_256 # KECCAK256 static cost |
| 55 | + + math.ceil(i / 32) * gsc.G_KECCAK_256_WORD # KECCAK256 dynamic cost |
| 56 | + + gsc.G_BASE # POP |
| 57 | + ) |
| 58 | + # From the available gas, we substract the mem expansion costs considering we know the |
| 59 | + # current input size length i. |
| 60 | + available_gas_after_expansion = max(0, available_gas - mem_exp_gas_calculator(new_bytes=i)) |
| 61 | + # Calculate how many calls we can do. |
| 62 | + num_keccak_calls = available_gas_after_expansion // iteration_gas_cost |
| 63 | + # KECCAK does 1 permutation every 136 bytes. |
| 64 | + num_keccak_permutations = num_keccak_calls * math.ceil(i / KECCAK_RATE) |
| 65 | + |
| 66 | + # If we found an input size that is better (reg permutations/gas), then save it. |
| 67 | + if num_keccak_permutations > max_keccak_perm_per_block: |
| 68 | + max_keccak_perm_per_block = num_keccak_permutations |
| 69 | + optimal_input_length = i |
| 70 | + |
| 71 | + # max_iters_loop contains how many keccak calls can be done per loop. |
| 72 | + # The loop is as big as possible bounded by the maximum code size. |
| 73 | + # |
| 74 | + # The loop structure is: JUMPDEST + [attack iteration] + PUSH0 + JUMP |
| 75 | + # |
| 76 | + # Now calculate available gas for [attack iteration]: |
| 77 | + # Numerator = MAX_CODE_SIZE-3. The -3 is for the JUMPDEST, PUSH0 and JUMP. |
| 78 | + # Denominator = (PUSHN + PUSH1 + KECCAK256 + POP) + PUSH1_DATA + PUSHN_DATA |
| 79 | + # TODO: the testing framework uses PUSH1(0) instead of PUSH0 which is suboptimal for the |
| 80 | + # attack, whenever this is fixed adjust accordingly. |
| 81 | + start_code = Op.JUMPDEST + Op.PUSH20[optimal_input_length] |
| 82 | + loop_code = Op.POP(Op.SHA3(Op.PUSH0, Op.DUP1)) |
| 83 | + end_code = Op.POP + Op.JUMP(Op.PUSH0) |
| 84 | + max_iters_loop = (MAX_CODE_SIZE - (len(start_code) + len(end_code))) // len(loop_code) |
| 85 | + code = start_code + (loop_code * max_iters_loop) + end_code |
| 86 | + if len(code) > MAX_CODE_SIZE: |
| 87 | + # Must never happen, but keep it as a sanity check. |
| 88 | + raise ValueError(f"Code size {len(code)} exceeds maximum code size {MAX_CODE_SIZE}") |
| 89 | + |
| 90 | + code_address = pre.deploy_contract(code=bytes(code)) |
| 91 | + |
| 92 | + tx = Transaction( |
| 93 | + to=code_address, |
| 94 | + gas_limit=gas_limit, |
| 95 | + gas_price=10, |
| 96 | + sender=pre.fund_eoa(), |
| 97 | + data=[], |
| 98 | + value=0, |
| 99 | + ) |
| 100 | + |
| 101 | + blockchain_test( |
| 102 | + env=env, |
| 103 | + pre=pre, |
| 104 | + post={}, |
| 105 | + blocks=[Block(txs=[tx])], |
| 106 | + ) |
0 commit comments