diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c2bc908362e..f24e5611029 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,8 @@ Test fixtures for use by clients are available for each release on the [Github r ### 🛠️ Framework +- ✨ Add benchmark-specific test wrapper (`benchmark_test`) that supports **EIP-7825** and create a benchmark code generator for common test pattern ([#1945](https://github.com/ethereum/execution-spec-tests/pull/1945)). + #### `fill` - Move pytest marker registration for `fill` and `execute-*` from their respective ini files to the shared `pytest_plugins.shared.execute_fill` pytest plugin ([#2110](https://github.com/ethereum/execution-spec-tests/pull/2110)). diff --git a/src/ethereum_test_benchmark/__init__.py b/src/ethereum_test_benchmark/__init__.py new file mode 100644 index 00000000000..60f0e66a5fb --- /dev/null +++ b/src/ethereum_test_benchmark/__init__.py @@ -0,0 +1,13 @@ +"""Benchmark code generator classes for creating optimized bytecode patterns.""" + +from .benchmark_code_generator import ( + BenchmarkCodeGenerator, + ExtCallGenerator, + JumpLoopGenerator, +) + +__all__ = ( + "BenchmarkCodeGenerator", + "ExtCallGenerator", + "JumpLoopGenerator", +) diff --git a/src/ethereum_test_benchmark/benchmark_code_generator.py b/src/ethereum_test_benchmark/benchmark_code_generator.py new file mode 100644 index 00000000000..9c2c9b7814a --- /dev/null +++ b/src/ethereum_test_benchmark/benchmark_code_generator.py @@ -0,0 +1,68 @@ +"""Benchmark code generator classes for creating optimized bytecode patterns.""" + +from ethereum_test_forks import Fork +from ethereum_test_specs.benchmark import BenchmarkCodeGenerator +from ethereum_test_types import Alloc, Transaction +from ethereum_test_vm import Bytecode +from ethereum_test_vm.opcodes import Opcodes as Op + + +class JumpLoopGenerator(BenchmarkCodeGenerator): + """Generates bytecode that loops execution using JUMP operations.""" + + def deploy_contracts(self, pre: Alloc, fork: Fork) -> None: + """Deploy the looping contract.""" + # Benchmark Test Structure: + # setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_length) + code = self.generate_repeated_code(self.attack_block, self.setup, fork) + self._contract_address = pre.deploy_contract(code=code) + + def generate_transaction(self, pre: Alloc, gas_limit: int, fork: Fork) -> Transaction: + """Generate transaction that executes the looping contract.""" + if not hasattr(self, "_contract_address"): + raise ValueError("deploy_contracts must be called before generate_transaction") + + return Transaction( + to=self._contract_address, + gas_limit=gas_limit, + sender=pre.fund_eoa(), + ) + + +class ExtCallGenerator(BenchmarkCodeGenerator): + """Generates bytecode that fills the contract to maximum allowed code size.""" + + def deploy_contracts(self, pre: Alloc, fork: Fork) -> None: + """Deploy both target and caller contracts.""" + # Benchmark Test Structure: + # There are two contracts: + # 1. The target contract that executes certain operation but not loop (e.g. PUSH) + # 2. The loop contract that calls the target contract in a loop + + max_iterations = min( + fork.max_stack_height(), fork.max_code_size() // len(self.attack_block) + ) + + # Deploy target contract that contains the actual attack block + self._target_contract_address = pre.deploy_contract( + code=self.attack_block * max_iterations + ) + + # Create caller contract that repeatedly calls the target contract + # attack = POP(STATICCALL(GAS, target_contract_address, 0, 0, 0, 0)) + # setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_length) + code_sequence = Op.POP(Op.STATICCALL(Op.GAS, self._target_contract_address, 0, 0, 0, 0)) + + caller_code = self.generate_repeated_code(code_sequence, Bytecode(), fork) + self._contract_address = pre.deploy_contract(code=caller_code) + + def generate_transaction(self, pre: Alloc, gas_limit: int, fork: Fork) -> Transaction: + """Generate transaction that executes the caller contract.""" + if not hasattr(self, "_contract_address"): + raise ValueError("deploy_contracts must be called before generate_transaction") + + return Transaction( + to=self._contract_address, + gas_limit=gas_limit, + sender=pre.fund_eoa(), + ) diff --git a/src/ethereum_test_specs/__init__.py b/src/ethereum_test_specs/__init__.py index 790e2b4351f..e0baf8c5188 100644 --- a/src/ethereum_test_specs/__init__.py +++ b/src/ethereum_test_specs/__init__.py @@ -2,6 +2,7 @@ from .base import BaseTest, TestSpec from .base_static import BaseStaticTest +from .benchmark import BenchmarkTest, BenchmarkTestFiller, BenchmarkTestSpec from .blobs import BlobsTest, BlobsTestFiller, BlobsTestSpec from .blockchain import ( BlockchainTest, @@ -23,6 +24,9 @@ __all__ = ( "BaseStaticTest", "BaseTest", + "BenchmarkTest", + "BenchmarkTestFiller", + "BenchmarkTestSpec", "BlobsTest", "BlobsTestFiller", "BlobsTestSpec", diff --git a/src/ethereum_test_specs/benchmark.py b/src/ethereum_test_specs/benchmark.py new file mode 100644 index 00000000000..440faa8b844 --- /dev/null +++ b/src/ethereum_test_specs/benchmark.py @@ -0,0 +1,257 @@ +"""Ethereum benchmark test spec definition and filler.""" + +import math +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Callable, ClassVar, Dict, Generator, List, Sequence, Type + +import pytest +from pydantic import ConfigDict, Field + +from ethereum_clis import TransitionTool +from ethereum_test_base_types import HexNumber +from ethereum_test_exceptions import BlockException, TransactionException +from ethereum_test_execution import ( + BaseExecute, + ExecuteFormat, + LabeledExecuteFormat, + TransactionPost, +) +from ethereum_test_fixtures import ( + BaseFixture, + BlockchainEngineFixture, + BlockchainEngineXFixture, + BlockchainFixture, + FixtureFormat, + LabeledFixtureFormat, +) +from ethereum_test_forks import Fork +from ethereum_test_types import Alloc, Environment, Transaction +from ethereum_test_vm import Bytecode +from ethereum_test_vm.opcodes import Opcodes as Op + +from .base import BaseTest +from .blockchain import Block, BlockchainTest + + +@dataclass(kw_only=True) +class BenchmarkCodeGenerator(ABC): + """Abstract base class for generating benchmark bytecode.""" + + attack_block: Bytecode + setup: Bytecode = field(default_factory=Bytecode) + + @abstractmethod + def deploy_contracts(self, pre: Alloc, fork: Fork) -> None: + """Deploy any contracts needed for the benchmark.""" + ... + + @abstractmethod + def generate_transaction(self, pre: Alloc, gas_limit: int, fork: Fork) -> Transaction: + """Generate a transaction with the specified gas limit.""" + ... + + def generate_repeated_code( + self, repeated_code: Bytecode, setup: Bytecode, fork: Fork + ) -> Bytecode: + """Calculate the maximum number of iterations that can fit in the code size limit.""" + assert len(repeated_code) > 0, "repeated_code cannot be empty" + max_code_size = fork.max_code_size() + + overhead = len(setup) + len(Op.JUMPDEST) + len(Op.JUMP(len(setup))) + available_space = max_code_size - overhead + max_iterations = available_space // len(repeated_code) + + code = setup + Op.JUMPDEST + repeated_code * max_iterations + Op.JUMP(len(setup)) + self._validate_code_size(code, fork) + + return code + + def _validate_code_size(self, code: Bytecode, fork: Fork) -> None: + """Validate that the generated code fits within size limits.""" + if len(code) > fork.max_code_size(): + raise ValueError( + f"Generated code size {len(code)} exceeds maximum allowed size " + f"{fork.max_code_size()}" + ) + + +class BenchmarkTest(BaseTest): + """Test type designed specifically for benchmark test cases.""" + + model_config = ConfigDict(extra="forbid") + + pre: Alloc + post: Alloc = Field(default_factory=Alloc) + tx: Transaction | None = None + blocks: List[Block] | None = None + block_exception: ( + List[TransactionException | BlockException] | TransactionException | BlockException | None + ) = None + env: Environment = Field(default_factory=Environment) + expected_benchmark_gas_used: int | None = None + gas_benchmark_value: int = Field(default_factory=lambda: int(Environment().gas_limit)) + code_generator: BenchmarkCodeGenerator | None = None + + supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [ + BlockchainFixture, + BlockchainEngineFixture, + BlockchainEngineXFixture, + ] + + supported_execute_formats: ClassVar[Sequence[LabeledExecuteFormat]] = [ + LabeledExecuteFormat( + TransactionPost, + "benchmark_test", + "An execute test derived from a benchmark test", + ), + ] + + supported_markers: ClassVar[Dict[str, str]] = { + "blockchain_test_engine_only": "Only generate a blockchain test engine fixture", + "blockchain_test_only": "Only generate a blockchain test fixture", + } + + @classmethod + def pytest_parameter_name(cls) -> str: + """Return the parameter name used in pytest to select this spec type.""" + return "benchmark_test" + + @classmethod + def discard_fixture_format_by_marks( + cls, + fixture_format: FixtureFormat, + fork: Fork, + markers: List[pytest.Mark], + ) -> bool: + """Discard a fixture format from filling if the appropriate marker is used.""" + if "blockchain_test_only" in [m.name for m in markers]: + return fixture_format != BlockchainFixture + if "blockchain_test_engine_only" in [m.name for m in markers]: + return fixture_format != BlockchainEngineFixture + return False + + def get_genesis_environment(self, fork: Fork) -> Environment: + """Get the genesis environment for this benchmark test.""" + return self.env + + def split_transaction(self, tx: Transaction, gas_limit_cap: int | None) -> List[Transaction]: + """Split a transaction that exceeds the gas limit cap into multiple transactions.""" + if gas_limit_cap is None: + tx.gas_limit = HexNumber(self.gas_benchmark_value) + return [tx] + + if gas_limit_cap >= self.gas_benchmark_value: + tx.gas_limit = HexNumber(self.gas_benchmark_value) + return [tx] + + num_splits = math.ceil(self.gas_benchmark_value / gas_limit_cap) + remaining_gas = self.gas_benchmark_value + + split_transactions = [] + for i in range(num_splits): + split_tx = tx.model_copy() + split_tx.gas_limit = HexNumber(remaining_gas if i == num_splits - 1 else gas_limit_cap) + remaining_gas -= gas_limit_cap + split_tx.nonce = HexNumber(tx.nonce + i) + split_transactions.append(split_tx) + + return split_transactions + + def generate_blocks_from_code_generator(self, fork: Fork) -> List[Block]: + """Generate blocks using the code generator.""" + if self.code_generator is None: + raise Exception("Code generator is not set") + + self.code_generator.deploy_contracts(self.pre, fork) + gas_limit = fork.transaction_gas_limit_cap() or self.gas_benchmark_value + benchmark_tx = self.code_generator.generate_transaction(self.pre, gas_limit, fork) + + execution_txs = self.split_transaction(benchmark_tx, gas_limit) + execution_block = Block(txs=execution_txs) + + return [execution_block] + + def generate_blockchain_test(self, fork: Fork) -> BlockchainTest: + """Create a BlockchainTest from this BenchmarkTest.""" + set_props = [ + name + for name, val in [ + ("code_generator", self.code_generator), + ("blocks", self.blocks), + ("tx", self.tx), + ] + if val is not None + ] + + if len(set_props) != 1: + raise ValueError( + f"Exactly one must be set, but got {len(set_props)}: {', '.join(set_props)}" + ) + + if self.code_generator is not None: + generated_blocks = self.generate_blocks_from_code_generator(fork) + return BlockchainTest.from_test( + base_test=self, + genesis_environment=self.env, + pre=self.pre, + post=self.post, + blocks=generated_blocks, + ) + elif self.blocks is not None: + return BlockchainTest.from_test( + base_test=self, + genesis_environment=self.env, + pre=self.pre, + post=self.post, + blocks=self.blocks, + ) + elif self.tx is not None: + gas_limit = fork.transaction_gas_limit_cap() or self.gas_benchmark_value + + transactions = self.split_transaction(self.tx, gas_limit) + + blocks = [Block(txs=transactions)] + + return BlockchainTest.from_test( + base_test=self, + pre=self.pre, + post=self.post, + blocks=blocks, + genesis_environment=self.env, + ) + else: + raise ValueError("Cannot create BlockchainTest without transactions or blocks") + + def generate( + self, + t8n: TransitionTool, + fork: Fork, + fixture_format: FixtureFormat, + ) -> BaseFixture: + """Generate the blockchain test fixture.""" + self.check_exception_test(exception=self.tx.error is not None if self.tx else False) + if fixture_format in BlockchainTest.supported_fixture_formats: + return self.generate_blockchain_test(fork=fork).generate( + t8n=t8n, fork=fork, fixture_format=fixture_format + ) + else: + raise Exception(f"Unsupported fixture format: {fixture_format}") + + def execute( + self, + *, + fork: Fork, + execute_format: ExecuteFormat, + ) -> BaseExecute: + """Execute the benchmark test by sending it to the live network.""" + if execute_format == TransactionPost: + return TransactionPost( + blocks=[[self.tx]], + post=self.post, + ) + raise Exception(f"Unsupported execute format: {execute_format}") + + +BenchmarkTestSpec = Callable[[str], Generator[BenchmarkTest, None, None]] +BenchmarkTestFiller = Type[BenchmarkTest] diff --git a/src/ethereum_test_specs/tests/test_benchmark.py b/src/ethereum_test_specs/tests/test_benchmark.py new file mode 100644 index 00000000000..bd4a699720b --- /dev/null +++ b/src/ethereum_test_specs/tests/test_benchmark.py @@ -0,0 +1,105 @@ +"""Tests for the BenchmarkTest class and its transaction splitting functionality.""" + +import pytest + +from ethereum_test_base_types import HexNumber +from ethereum_test_specs.benchmark import BenchmarkTest +from ethereum_test_types import Alloc, Environment, Transaction + + +@pytest.mark.parametrize( + "gas_benchmark_value_millions,expected_splits", + [ + (1, 1), # 1M / 16M = 1 transaction + (10, 1), # 10M / 16M = 1 transaction + (30, 2), # 30M / 16M = 2 transactions (16M + 14M) + (45, 3), # 45M / 16M = 3 transactions (16M + 16M + 13M) + (60, 4), # 60M / 16M = 4 transactions (16M + 16M + 16M + 12M) + (100, 7), # 100M / 16M = 7 transactions (6x16M + 4M) + (150, 10), # 150M / 16M = 10 transactions (9x16M + 6M) + ], +) +def test_split_transaction(gas_benchmark_value_millions: int, expected_splits: int): + """Test that transaction splitting works correctly for Osaka fork gas cap.""" + gas_benchmark_value = gas_benchmark_value_millions * 1_000_000 + gas_limit_cap = 16_000_000 # Osaka's transaction gas limit cap + + # Create a minimal BenchmarkTest instance + benchmark_test = BenchmarkTest( + pre=Alloc(), + post=Alloc(), + tx=Transaction(sender=HexNumber(0), to=HexNumber(0), nonce=0), + env=Environment(), + gas_benchmark_value=gas_benchmark_value, + ) + + # Test the split_transaction method + assert benchmark_test.tx is not None, "Transaction should not be None" + split_txs = benchmark_test.split_transaction(benchmark_test.tx, gas_limit_cap) + + # Verify the number of transactions + assert len(split_txs) == expected_splits, ( + f"Expected {expected_splits} transactions for {gas_benchmark_value_millions}M gas, " + f"got {len(split_txs)}" + ) + + # Verify total gas equals the benchmark value + total_gas = sum(tx.gas_limit for tx in split_txs) + assert total_gas == gas_benchmark_value, ( + f"Total gas {total_gas} doesn't match benchmark value {gas_benchmark_value}" + ) + + # Verify no transaction exceeds the cap + for i, tx in enumerate(split_txs): + assert tx.gas_limit <= gas_limit_cap, ( + f"Transaction {i} gas limit {tx.gas_limit} exceeds cap {gas_limit_cap}" + ) + + # Verify nonces increment correctly + for i, tx in enumerate(split_txs): + assert tx.nonce == i, f"Transaction {i} has incorrect nonce {tx.nonce}" + + # Verify gas distribution + for i, tx in enumerate(split_txs[:-1]): # All but last should be at cap + assert tx.gas_limit == gas_limit_cap, ( + f"Transaction {i} should have gas limit {gas_limit_cap}, got {tx.gas_limit}" + ) + + # Last transaction should have the remainder + if expected_splits > 1: + expected_last_gas = gas_benchmark_value - (gas_limit_cap * (expected_splits - 1)) + assert split_txs[-1].gas_limit == expected_last_gas, ( + f"Last transaction should have {expected_last_gas} gas, got {split_txs[-1].gas_limit}" + ) + + +@pytest.mark.parametrize( + "gas_benchmark_value,gas_limit_cap", + [ + (50_000_000, None), # No cap - should return single transaction + (50_000_000, 100_000_000), # Cap higher than benchmark value + ], +) +def test_split_transaction_edge_cases(gas_benchmark_value: int, gas_limit_cap: int | None): + """Test edge cases for transaction splitting.""" + benchmark_test = BenchmarkTest( + pre=Alloc(), + post=Alloc(), + tx=Transaction(sender=HexNumber(0), to=HexNumber(0), nonce=0, gas_limit=1_000_000_000), + env=Environment(), + gas_benchmark_value=gas_benchmark_value, + ) + + assert benchmark_test.tx is not None, "Transaction should not be None" + split_txs = benchmark_test.split_transaction(benchmark_test.tx, gas_limit_cap) + + # Should return single transaction in both cases + assert len(split_txs) == 1, f"Expected 1 transaction, got {len(split_txs)}" + + if gas_limit_cap is None: + # When no cap, gas_limit should be benchmark value + assert split_txs[0].gas_limit == gas_benchmark_value + else: + # When cap > benchmark, gas_limit should be min of tx.gas_limit and benchmark + assert benchmark_test.tx is not None, "Transaction should not be None" + assert split_txs[0].gas_limit == min(benchmark_test.tx.gas_limit, gas_benchmark_value) diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index bb0b026ef9e..fc8057c0017 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -16,6 +16,11 @@ TestPrivateKey2, ) from ethereum_test_base_types.reference_spec import ReferenceSpec, ReferenceSpecTypes +from ethereum_test_benchmark import ( + BenchmarkCodeGenerator, + ExtCallGenerator, + JumpLoopGenerator, +) from ethereum_test_exceptions import ( BlockException, EngineAPIError, @@ -25,6 +30,8 @@ from ethereum_test_fixtures import BaseFixture, FixtureCollector from ethereum_test_specs import ( BaseTest, + BenchmarkTest, + BenchmarkTestFiller, BlobsTest, BlobsTestFiller, BlockchainTest, @@ -112,6 +119,9 @@ "BalStorageSlot", "BaseFixture", "BaseTest", + "BenchmarkCodeGenerator", + "BenchmarkTest", + "BenchmarkTestFiller", "Blob", "BlockAccessList", "BlobsTest", @@ -128,6 +138,7 @@ "CodeGasMeasure", "Conditional", "ConsolidationRequest", + "ExtCallGenerator", "DeploymentTestType", "DepositRequest", "EngineAPIError", @@ -143,6 +154,7 @@ "Hash", "Header", "Initcode", + "JumpLoopGenerator", "Macro", "Macros", "NetworkWrappedTransaction", diff --git a/src/ethereum_test_vm/bytecode.py b/src/ethereum_test_vm/bytecode.py index e07ab2cad0e..5eea5b0cce3 100644 --- a/src/ethereum_test_vm/bytecode.py +++ b/src/ethereum_test_vm/bytecode.py @@ -1,6 +1,13 @@ """Ethereum Virtual Machine bytecode primitives and utilities.""" -from typing import SupportsBytes +from typing import Any, SupportsBytes + +from pydantic import GetCoreSchemaHandler +from pydantic_core.core_schema import ( + PlainValidatorFunctionSchema, + no_info_plain_validator_function, + plain_serializer_function_ser_schema, +) from ethereum_test_base_types import Bytes, Hash @@ -217,3 +224,16 @@ def hex(self) -> str: def keccak256(self) -> Hash: """Return the keccak256 hash of the opcode byte representation.""" return Bytes(self._bytes_).keccak256() + + @classmethod + def __get_pydantic_core_schema__( + cls, source_type: Any, handler: GetCoreSchemaHandler + ) -> PlainValidatorFunctionSchema: + """Provide Pydantic core schema for Bytecode serialization and validation.""" + return no_info_plain_validator_function( + cls, + serialization=plain_serializer_function_ser_schema( + lambda bytecode: "0x" + bytecode.hex(), + info_arg=False, + ), + ) diff --git a/src/pytest_plugins/shared/execute_fill.py b/src/pytest_plugins/shared/execute_fill.py index 98fc765db07..21b9b7ea5ba 100644 --- a/src/pytest_plugins/shared/execute_fill.py +++ b/src/pytest_plugins/shared/execute_fill.py @@ -13,6 +13,7 @@ from ..spec_version_checker.spec_version_checker import EIPSpecTestItem ALL_FIXTURE_PARAMETERS = { + "gas_benchmark_value", "genesis_environment", "env", } diff --git a/tests/benchmark/conftest.py b/tests/benchmark/conftest.py index 3af1bf9ade7..1e2e7813817 100644 --- a/tests/benchmark/conftest.py +++ b/tests/benchmark/conftest.py @@ -4,6 +4,8 @@ import pytest +from ethereum_test_forks import Fork + DEFAULT_BENCHMARK_FORK = "Prague" @@ -59,3 +61,9 @@ def pytest_collection_modifyitems(config, items): for i in reversed(items_for_removal): items.pop(i) + + +@pytest.fixture +def tx_gas_limit_cap(fork: Fork, gas_benchmark_value: int) -> int: + """Return the transaction gas limit cap.""" + return fork.transaction_gas_limit_cap() or gas_benchmark_value diff --git a/tests/benchmark/test_worst_blocks.py b/tests/benchmark/test_worst_blocks.py index 38e6d5f71e6..b71f3035067 100644 --- a/tests/benchmark/test_worst_blocks.py +++ b/tests/benchmark/test_worst_blocks.py @@ -9,14 +9,15 @@ import pytest +from ethereum_test_base_types import Account from ethereum_test_forks import Fork from ethereum_test_tools import ( AccessList, - Account, Address, Alloc, Block, BlockchainTestFiller, + Environment, Hash, StateTestFiller, Transaction, @@ -112,11 +113,13 @@ def ether_transfer_case( def test_block_full_of_ether_transfers( blockchain_test: BlockchainTestFiller, pre: Alloc, + env: Environment, case_id: str, ether_transfer_case, iteration_count: int, transfer_amount: int, intrinsic_cost: int, + gas_benchmark_value: int, ): """ Single test for ether transfer scenarios. @@ -153,10 +156,10 @@ def test_block_full_of_ether_transfers( ) blockchain_test( + genesis_environment=env, pre=pre, post=post_state, blocks=[Block(txs=txs)], - exclude_full_post_state_in_output=True, expected_benchmark_gas_used=iteration_count * intrinsic_cost, ) diff --git a/tests/benchmark/test_worst_compute.py b/tests/benchmark/test_worst_compute.py index 9bfdee16482..d7437a77e4c 100644 --- a/tests/benchmark/test_worst_compute.py +++ b/tests/benchmark/test_worst_compute.py @@ -15,10 +15,12 @@ from py_ecc.bn128 import G1, G2, multiply from ethereum_test_base_types.base_types import Bytes +from ethereum_test_benchmark.benchmark_code_generator import JumpLoopGenerator from ethereum_test_forks import Fork from ethereum_test_tools import ( Address, Alloc, + BenchmarkTestFiller, Block, BlockchainTestFiller, Bytecode, @@ -1842,30 +1844,14 @@ def test_worst_jumpis( @pytest.mark.slow def test_worst_jumpdests( - state_test: StateTestFiller, + benchmark_test: BenchmarkTestFiller, pre: Alloc, - fork: Fork, - gas_benchmark_value: int, ): """Test running a JUMPDEST-intensive contract.""" - max_code_size = fork.max_code_size() - - # Create and deploy a contract with many JUMPDESTs - code_suffix = Op.JUMP(Op.PUSH0) - code_body = Op.JUMPDEST * (max_code_size - len(code_suffix)) - code = code_body + code_suffix - jumpdests_address = pre.deploy_contract(code=code) - - tx = Transaction( - to=jumpdests_address, - gas_limit=gas_benchmark_value, - sender=pre.fund_eoa(), - ) - - state_test( + benchmark_test( pre=pre, post={}, - tx=tx, + code_generator=JumpLoopGenerator(attack_block=Op.JUMPDEST), ) @@ -2764,31 +2750,17 @@ def test_worst_calldataload( ], ) def test_worst_swap( - state_test: StateTestFiller, + benchmark_test: BenchmarkTestFiller, pre: Alloc, - fork: Fork, opcode: Opcode, - gas_benchmark_value: int, ): """Test running a block with as many SWAP as possible.""" - max_code_size = fork.max_code_size() - - code_prefix = Op.JUMPDEST + Op.PUSH0 * opcode.min_stack_height - code_suffix = Op.PUSH0 + Op.JUMP - opcode_sequence = opcode * (max_code_size - len(code_prefix) - len(code_suffix)) - code = code_prefix + opcode_sequence + code_suffix - assert len(code) <= max_code_size - - tx = Transaction( - to=pre.deploy_contract(code=code), - gas_limit=gas_benchmark_value, - sender=pre.fund_eoa(), - ) - - state_test( + benchmark_test( pre=pre, post={}, - tx=tx, + code_generator=JumpLoopGenerator( + attack_block=opcode, setup=Op.PUSH0 * opcode.min_stack_height + ), )