|
| 1 | +"""Benchmark code generator classes for creating optimized bytecode patterns.""" |
| 2 | + |
| 3 | +from abc import ABC, abstractmethod |
| 4 | +from typing import Optional |
| 5 | + |
| 6 | +from ethereum_test_forks import Fork |
| 7 | +from ethereum_test_tools import Alloc, Bytecode, Transaction |
| 8 | +from ethereum_test_tools.vm.opcode import Opcodes as Op |
| 9 | + |
| 10 | + |
| 11 | +class BenchmarkCodeGenerator(ABC): |
| 12 | + """Abstract base class for generating benchmark bytecode.""" |
| 13 | + |
| 14 | + def __init__( |
| 15 | + self, |
| 16 | + fork: Fork, |
| 17 | + attack_block: Bytecode, |
| 18 | + setup: Optional[Bytecode] = None, |
| 19 | + ): |
| 20 | + """Initialize with fork, attack block, and optional setup bytecode.""" |
| 21 | + self.fork = fork |
| 22 | + self.setup = setup or Bytecode() |
| 23 | + self.attack_block = attack_block |
| 24 | + |
| 25 | + @abstractmethod |
| 26 | + def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction: |
| 27 | + """Generate a transaction with the specified gas limit.""" |
| 28 | + pass |
| 29 | + |
| 30 | + def generate_repeated_code(self, repeated_code: Bytecode, setup: Bytecode) -> Bytecode: |
| 31 | + """Calculate the maximum number of iterations that can fit in the code size limit.""" |
| 32 | + max_code_size = self.fork.max_code_size() |
| 33 | + |
| 34 | + overhead = len(Op.JUMPDEST) + len(Op.JUMP(len(setup))) |
| 35 | + available_space = max_code_size - overhead |
| 36 | + max_iterations = available_space // len(repeated_code) if len(repeated_code) > 0 else 0 |
| 37 | + |
| 38 | + code = setup + Op.JUMPDEST + repeated_code * max_iterations + Op.JUMP(len(setup)) |
| 39 | + |
| 40 | + self._validate_code_size(code) |
| 41 | + |
| 42 | + return code |
| 43 | + |
| 44 | + def _validate_code_size(self, code: Bytecode) -> None: |
| 45 | + """Validate that the generated code fits within size limits.""" |
| 46 | + if len(code) > self.fork.max_code_size(): |
| 47 | + raise ValueError( |
| 48 | + f"Generated code size {len(code)} exceeds maximum allowed size " |
| 49 | + f"{self.fork.max_code_size()}" |
| 50 | + ) |
| 51 | + |
| 52 | + |
| 53 | +class JumpLoopGenerator(BenchmarkCodeGenerator): |
| 54 | + """Generates bytecode that loops execution using JUMP operations.""" |
| 55 | + |
| 56 | + def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction: |
| 57 | + """Generate transaction with looping bytecode pattern.""" |
| 58 | + # Benchmark Test Structure: |
| 59 | + # setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_length) |
| 60 | + |
| 61 | + code = self.generate_repeated_code(self.attack_block, self.setup) |
| 62 | + |
| 63 | + return Transaction( |
| 64 | + to=pre.deploy_contract(code=code), |
| 65 | + gas_limit=self.fork.transaction_gas_limit_cap() or 30_000_000, |
| 66 | + sender=pre.fund_eoa(), |
| 67 | + ) |
| 68 | + |
| 69 | + |
| 70 | +class ExtCallGenerator(BenchmarkCodeGenerator): |
| 71 | + """Generates bytecode that fills the contract to maximum allowed code size.""" |
| 72 | + |
| 73 | + def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction: |
| 74 | + """Generate transaction with maximal code size coverage.""" |
| 75 | + # Benchmark Test Structure: |
| 76 | + # There are two contracts: |
| 77 | + # 1. The target contract that executes certain operation but not loop (e.g. PUSH) |
| 78 | + # 2. The loop contract that calls the target contract in a loop |
| 79 | + # |
| 80 | + # attack = POP(STATICCALL(GAS, target_contract_address, 0, 0, 0, 0)) |
| 81 | + # setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_lengt) |
| 82 | + # This could optimize the gas consumption and increase the cycle count. |
| 83 | + |
| 84 | + max_stack_height = self.fork.max_stack_height() |
| 85 | + |
| 86 | + target_contract_address = pre.deploy_contract(code=self.attack_block * max_stack_height) |
| 87 | + |
| 88 | + code_sequence = Op.POP(Op.STATICCALL(Op.GAS, target_contract_address, 0, 0, 0, 0)) |
| 89 | + |
| 90 | + code = self.generate_repeated_code(code_sequence, Bytecode()) |
| 91 | + |
| 92 | + return Transaction( |
| 93 | + to=pre.deploy_contract(code=code), |
| 94 | + gas_limit=self.fork.transaction_gas_limit_cap() or 30_000_000, |
| 95 | + sender=pre.fund_eoa(), |
| 96 | + ) |
0 commit comments