From 7d77562cbc1e7aebff583abc22f73c1a7999f35d Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Fri, 8 Aug 2025 18:06:06 +0800 Subject: [PATCH 1/3] feat: wrap blockchain test for benchmark --- src/ethereum_test_specs/__init__.py | 4 + src/ethereum_test_specs/benchmark.py | 164 +++++++++++++++++++++++++++ src/ethereum_test_tools/__init__.py | 4 + tests/benchmark/test_worst_blocks.py | 9 +- 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 src/ethereum_test_specs/benchmark.py 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..fb0eb8b68f1 --- /dev/null +++ b/src/ethereum_test_specs/benchmark.py @@ -0,0 +1,164 @@ +"""Ethereum benchmark test spec definition and filler.""" + +from typing import Callable, ClassVar, Dict, Generator, List, Optional, Sequence, Type + +import pytest +from pydantic import 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 .base import BaseTest +from .blockchain import Block, BlockchainTest + + +class BenchmarkTest(BaseTest): + """Test type designed specifically for benchmark test cases.""" + + pre: Alloc + post: Alloc + tx: Optional[Transaction] = None + blocks: Optional[List[Block]] = None + block_exception: ( + List[TransactionException | BlockException] | TransactionException | BlockException | None + ) = None + env: Environment = Field(default_factory=Environment) + expected_benchmark_gas_used: int | 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) or (tx.gas_limit <= gas_limit_cap): + return [tx] + + total_gas = int(self.expected_benchmark_gas_used or self.env.gas_limit) + print(f"total_gas: {total_gas}") + num_splits = total_gas // gas_limit_cap + + split_transactions = [] + for i in range(num_splits): + split_tx = tx.model_copy() + total_gas -= gas_limit_cap + split_tx.gas_limit = HexNumber(total_gas if i == num_splits - 1 else gas_limit_cap) + split_tx.nonce = HexNumber(tx.nonce + i) + split_transactions.append(split_tx) + + return split_transactions + + def generate_blockchain_test(self, fork: Fork) -> BlockchainTest: + """Create a BlockchainTest from this BenchmarkTest.""" + if 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_cap = fork.transaction_gas_limit_cap() + + transactions = self.split_transaction(self.tx, gas_limit_cap) + + 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_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 1617bf76ef5..9caa1239ad1 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -25,6 +25,8 @@ from ethereum_test_fixtures import BaseFixture, FixtureCollector from ethereum_test_specs import ( BaseTest, + BenchmarkTest, + BenchmarkTestFiller, BlobsTest, BlobsTestFiller, BlockchainTest, @@ -98,6 +100,8 @@ "AuthorizationTuple", "BaseFixture", "BaseTest", + "BenchmarkTest", + "BenchmarkTestFiller", "Blob", "BlobsTest", "BlobsTestFiller", diff --git a/tests/benchmark/test_worst_blocks.py b/tests/benchmark/test_worst_blocks.py index 327cbae00c9..133761a445f 100644 --- a/tests/benchmark/test_worst_blocks.py +++ b/tests/benchmark/test_worst_blocks.py @@ -15,8 +15,9 @@ Account, Address, Alloc, + BenchmarkTestFiller, Block, - BlockchainTestFiller, + Environment, Hash, StateTestFiller, Transaction, @@ -111,8 +112,9 @@ def ether_transfer_case( ["a_to_a", "a_to_b", "diff_acc_to_b", "a_to_diff_acc", "diff_acc_to_diff_acc"], ) def test_block_full_of_ether_transfers( - blockchain_test: BlockchainTestFiller, + benchmark_test: BenchmarkTestFiller, pre: Alloc, + env: Environment, case_id: str, ether_transfer_case, iteration_count: int, @@ -153,7 +155,8 @@ def test_block_full_of_ether_transfers( else {receiver: Account(balance=balance) for receiver, balance in balances.items()} ) - blockchain_test( + benchmark_test( + genesis_environment=env, pre=pre, post=post_state, blocks=[Block(txs=txs)], From 0ef971dc24be0b64d920f4efd08846e66af9801c Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Fri, 8 Aug 2025 18:06:30 +0800 Subject: [PATCH 2/3] feat: wrap state test for benchmark --- src/ethereum_test_specs/__init__.py | 4 + src/ethereum_test_specs/benchmark_state.py | 229 +++++++++++++++++++++ src/ethereum_test_tools/__init__.py | 4 + tests/benchmark/test_worst_compute.py | 8 +- 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 src/ethereum_test_specs/benchmark_state.py diff --git a/src/ethereum_test_specs/__init__.py b/src/ethereum_test_specs/__init__.py index e0baf8c5188..9a714640746 100644 --- a/src/ethereum_test_specs/__init__.py +++ b/src/ethereum_test_specs/__init__.py @@ -3,6 +3,7 @@ from .base import BaseTest, TestSpec from .base_static import BaseStaticTest from .benchmark import BenchmarkTest, BenchmarkTestFiller, BenchmarkTestSpec +from .benchmark_state import BenchmarkStateTest, BenchmarkStateTestFiller, BenchmarkStateTestSpec from .blobs import BlobsTest, BlobsTestFiller, BlobsTestSpec from .blockchain import ( BlockchainTest, @@ -27,6 +28,9 @@ "BenchmarkTest", "BenchmarkTestFiller", "BenchmarkTestSpec", + "BenchmarkStateTest", + "BenchmarkStateTestFiller", + "BenchmarkStateTestSpec", "BlobsTest", "BlobsTestFiller", "BlobsTestSpec", diff --git a/src/ethereum_test_specs/benchmark_state.py b/src/ethereum_test_specs/benchmark_state.py new file mode 100644 index 00000000000..e9e959f0615 --- /dev/null +++ b/src/ethereum_test_specs/benchmark_state.py @@ -0,0 +1,229 @@ +"""Ethereum benchmark state test spec definition and filler.""" + +import math +from pprint import pprint +from typing import Callable, ClassVar, Generator, List, Sequence, Type + +from pydantic import ConfigDict + +from ethereum_clis import TransitionTool +from ethereum_test_base_types import HexNumber +from ethereum_test_execution import ( + BaseExecute, + ExecuteFormat, + LabeledExecuteFormat, + TransactionPost, +) +from ethereum_test_fixtures import ( + BaseFixture, + FixtureFormat, + LabeledFixtureFormat, + StateFixture, +) +from ethereum_test_fixtures.common import FixtureBlobSchedule +from ethereum_test_fixtures.state import ( + FixtureConfig, + FixtureEnvironment, + FixtureForkPost, + FixtureTransaction, +) +from ethereum_test_forks import Fork +from ethereum_test_types import Alloc, Environment, Transaction +from ethereum_test_vm import Bytecode + +from .base import BaseTest, OpMode +from .blockchain import Block, BlockchainTest +from .debugging import print_traces +from .helpers import verify_transactions + + +class BenchmarkStateTest(BaseTest): + """Test type designed specifically for benchmark state test cases with full verification.""" + + pre: Alloc + post: Alloc + tx: Transaction + gas_benchmark_value: int + setup_bytecode: Bytecode | None = None + attack_bytecode: Bytecode | None = None + env: Environment + chain_id: int = 1 + + model_config = ConfigDict(arbitrary_types_allowed=True) + + supported_fixture_formats: ClassVar[Sequence[FixtureFormat | LabeledFixtureFormat]] = [ + StateFixture, + ] + [ + LabeledFixtureFormat( + fixture_format, + f"{fixture_format.format_name}_from_benchmark_state_test", + f"A {fixture_format.format_name} generated from a benchmark_state_test", + ) + for fixture_format in BlockchainTest.supported_fixture_formats + ] + + supported_execute_formats: ClassVar[Sequence[LabeledExecuteFormat]] = [ + LabeledExecuteFormat( + TransactionPost, + "benchmark_state_test_with_verification", + "An execute test derived from a benchmark state test with verification", + ), + ] + + 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) or (tx.gas_limit <= gas_limit_cap): + return [tx] + + total_gas = int(tx.gas_limit) + num_splits = math.ceil(total_gas / gas_limit_cap) + + split_transactions = [] + remaining_gas = total_gas + for i in range(num_splits): + split_tx = tx.model_copy() + split_tx.gas_limit = HexNumber(min(gas_limit_cap, remaining_gas)) + split_tx.nonce = HexNumber(tx.nonce + i) + split_transactions.append(split_tx) + remaining_gas -= gas_limit_cap + + return split_transactions + + def make_benchmark_state_test_fixture( + self, + t8n: TransitionTool, + fork: Fork, + ) -> StateFixture: + """Create a fixture from the benchmark state test definition with full verification.""" + # We can't generate a state test fixture that names a transition fork, + # so we get the fork at the block number and timestamp of the state test + fork = fork.fork_at(self.env.number, self.env.timestamp) + + env = self.env.set_fork_requirements(fork) + tx = self.tx.with_signature_and_sender(keep_secret_key=True) + pre_alloc = Alloc.merge( + Alloc.model_validate(fork.pre_allocation()), + self.pre, + ) + + # Verification 1: Check for empty accounts + if empty_accounts := pre_alloc.empty_accounts(): + raise Exception(f"Empty accounts in pre state: {empty_accounts}") + + transition_tool_output = t8n.evaluate( + transition_tool_data=TransitionTool.TransitionToolData( + alloc=pre_alloc, + txs=[tx], + env=env, + fork=fork, + chain_id=self.chain_id, + reward=0, # Reward on state tests is always zero + blob_schedule=fork.blob_schedule(), + state_test=True, + ), + debug_output_path=self.get_next_transition_tool_output_path(), + slow_request=self.is_tx_gas_heavy_test(), + ) + + # Verification 2: Post-allocation verification + try: + self.post.verify_post_alloc(transition_tool_output.alloc) + except Exception as e: + print_traces(t8n.get_traces()) + raise e + + # Verification 3: Transaction verification + try: + verify_transactions( + txs=[tx], + result=transition_tool_output.result, + transition_tool_exceptions_reliable=t8n.exception_mapper.reliable, + ) + except Exception as e: + print_traces(t8n.get_traces()) + pprint(transition_tool_output.result) + pprint(transition_tool_output.alloc) + raise e + + # Verification 4: Benchmark gas validation + if self._operation_mode == OpMode.BENCHMARKING: + expected_benchmark_gas_used = self.gas_benchmark_value + gas_used = int(transition_tool_output.result.gas_used) + assert expected_benchmark_gas_used is not None, "gas_benchmark_value is not set" + assert gas_used == expected_benchmark_gas_used, ( + f"gas_used ({gas_used}) does not match gas_benchmark_value " + f"({expected_benchmark_gas_used})" + f", difference: {gas_used - expected_benchmark_gas_used}" + ) + + return StateFixture( + env=FixtureEnvironment(**env.model_dump(exclude_none=True)), + pre=pre_alloc, + post={ + fork: [ + FixtureForkPost( + state_root=transition_tool_output.result.state_root, + logs_hash=transition_tool_output.result.logs_hash, + tx_bytes=tx.rlp(), + expect_exception=tx.error, + state=transition_tool_output.alloc, + ) + ] + }, + transaction=FixtureTransaction.from_transaction(tx), + config=FixtureConfig( + blob_schedule=FixtureBlobSchedule.from_blob_schedule(fork.blob_schedule()), + chain_id=self.chain_id, + ), + ) + + def generate_blockchain_test(self, fork: Fork) -> BlockchainTest: + """Create a BlockchainTest from this BenchmarkStateTestWithVerification.""" + gas_limit_cap = fork.transaction_gas_limit_cap() + + transactions = self.split_transaction(self.tx, gas_limit_cap) + + blocks = [Block(txs=transactions)] + + return BlockchainTest.from_test( + base_test=self, + pre=self.pre, + post=self.post, + blocks=blocks, + genesis_environment=self.env, + ) + + def generate( + self, + t8n: TransitionTool, + fork: Fork, + fixture_format: FixtureFormat, + ) -> BaseFixture: + """Generate the test fixture.""" + self.check_exception_test(exception=self.tx.error is not None) + if fixture_format in BlockchainTest.supported_fixture_formats: + return self.generate_blockchain_test(fork=fork).generate( + t8n=t8n, fork=fork, fixture_format=fixture_format + ) + elif fixture_format == StateFixture: + return self.make_benchmark_state_test_fixture(t8n, fork) + + raise Exception(f"Unknown fixture format: {fixture_format}") + + def execute( + self, + *, + fork: Fork, + execute_format: ExecuteFormat, + ) -> BaseExecute: + """Execute the benchmark state 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}") + + +BenchmarkStateTestFiller = Type[BenchmarkStateTest] +BenchmarkStateTestSpec = Callable[[str], Generator[BenchmarkStateTest, None, None]] diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 9caa1239ad1..47c4da0aa79 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -25,6 +25,8 @@ from ethereum_test_fixtures import BaseFixture, FixtureCollector from ethereum_test_specs import ( BaseTest, + BenchmarkStateTest, + BenchmarkStateTestFiller, BenchmarkTest, BenchmarkTestFiller, BlobsTest, @@ -102,6 +104,8 @@ "BaseTest", "BenchmarkTest", "BenchmarkTestFiller", + "BenchmarkStateTest", + "BenchmarkStateTestFiller", "Blob", "BlobsTest", "BlobsTestFiller", diff --git a/tests/benchmark/test_worst_compute.py b/tests/benchmark/test_worst_compute.py index d6c4aba7c5b..2f706113033 100644 --- a/tests/benchmark/test_worst_compute.py +++ b/tests/benchmark/test_worst_compute.py @@ -19,6 +19,7 @@ from ethereum_test_tools import ( Address, Alloc, + BenchmarkStateTestFiller, Block, BlockchainTestFiller, Bytecode, @@ -2761,8 +2762,9 @@ def test_worst_calldataload( ], ) def test_worst_swap( - state_test: StateTestFiller, + benchmark_state_test: BenchmarkStateTestFiller, pre: Alloc, + env: Environment, fork: Fork, opcode: Opcode, gas_benchmark_value: int, @@ -2782,8 +2784,10 @@ def test_worst_swap( sender=pre.fund_eoa(), ) - state_test( + benchmark_state_test( + env=env, pre=pre, + gas_benchmark_value=gas_benchmark_value, post={}, tx=tx, ) From de7f4857632b297d4a1779e95fc420a3fdb34e59 Mon Sep 17 00:00:00 2001 From: LouisTsai Date: Thu, 14 Aug 2025 20:48:59 +0800 Subject: [PATCH 3/3] feat(benchmark): add code generator to generate transaction --- src/ethereum_test_specs/benchmark.py | 4 +- src/ethereum_test_specs/benchmark_state.py | 3 - src/ethereum_test_tools/__init__.py | 8 ++ .../benchmark_code_generator.py | 96 +++++++++++++++++++ tests/benchmark/test_worst_compute.py | 24 ++--- 5 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 src/ethereum_test_tools/benchmark_code_generator.py diff --git a/src/ethereum_test_specs/benchmark.py b/src/ethereum_test_specs/benchmark.py index fb0eb8b68f1..d1ffdb306db 100644 --- a/src/ethereum_test_specs/benchmark.py +++ b/src/ethereum_test_specs/benchmark.py @@ -128,7 +128,9 @@ def generate_blockchain_test(self, fork: Fork) -> BlockchainTest: genesis_environment=self.env, ) else: - raise ValueError("Cannot create BlockchainTest without transactions or blocks") + raise ValueError( + "Cannot create BlockchainTest without transactions, blocks, or code_generator" + ) def generate( self, diff --git a/src/ethereum_test_specs/benchmark_state.py b/src/ethereum_test_specs/benchmark_state.py index e9e959f0615..454af1a3844 100644 --- a/src/ethereum_test_specs/benchmark_state.py +++ b/src/ethereum_test_specs/benchmark_state.py @@ -29,7 +29,6 @@ ) from ethereum_test_forks import Fork from ethereum_test_types import Alloc, Environment, Transaction -from ethereum_test_vm import Bytecode from .base import BaseTest, OpMode from .blockchain import Block, BlockchainTest @@ -44,8 +43,6 @@ class BenchmarkStateTest(BaseTest): post: Alloc tx: Transaction gas_benchmark_value: int - setup_bytecode: Bytecode | None = None - attack_bytecode: Bytecode | None = None env: Environment chain_id: int = 1 diff --git a/src/ethereum_test_tools/__init__.py b/src/ethereum_test_tools/__init__.py index 47c4da0aa79..5946cc0bf5f 100644 --- a/src/ethereum_test_tools/__init__.py +++ b/src/ethereum_test_tools/__init__.py @@ -78,6 +78,11 @@ call_return_code, ) +from .benchmark_code_generator import ( + BenchmarkCodeGenerator, + ExtCallGenerator, + JumpLoopGenerator, +) from .code import ( CalldataCase, Case, @@ -102,6 +107,7 @@ "AuthorizationTuple", "BaseFixture", "BaseTest", + "BenchmarkCodeGenerator", "BenchmarkTest", "BenchmarkTestFiller", "BenchmarkStateTest", @@ -120,6 +126,7 @@ "CodeGasMeasure", "Conditional", "ConsolidationRequest", + "ExtCallGenerator", "DeploymentTestType", "DepositRequest", "EngineAPIError", @@ -135,6 +142,7 @@ "Hash", "Header", "Initcode", + "JumpLoopGenerator", "Macro", "Macros", "NetworkWrappedTransaction", diff --git a/src/ethereum_test_tools/benchmark_code_generator.py b/src/ethereum_test_tools/benchmark_code_generator.py new file mode 100644 index 00000000000..57e7b0e1e4c --- /dev/null +++ b/src/ethereum_test_tools/benchmark_code_generator.py @@ -0,0 +1,96 @@ +"""Benchmark code generator classes for creating optimized bytecode patterns.""" + +from abc import ABC, abstractmethod +from typing import Optional + +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Bytecode, Transaction +from ethereum_test_tools.vm.opcode import Opcodes as Op + + +class BenchmarkCodeGenerator(ABC): + """Abstract base class for generating benchmark bytecode.""" + + def __init__( + self, + fork: Fork, + attack_block: Bytecode, + setup: Optional[Bytecode] = None, + ): + """Initialize with fork, attack block, and optional setup bytecode.""" + self.fork = fork + self.setup = setup or Bytecode() + self.attack_block = attack_block + + @abstractmethod + def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction: + """Generate a transaction with the specified gas limit.""" + pass + + def generate_repeated_code(self, repeated_code: Bytecode, setup: Bytecode) -> Bytecode: + """Calculate the maximum number of iterations that can fit in the code size limit.""" + max_code_size = self.fork.max_code_size() + + overhead = len(Op.JUMPDEST) + len(Op.JUMP(len(setup))) + available_space = max_code_size - overhead + max_iterations = available_space // len(repeated_code) if len(repeated_code) > 0 else 0 + + code = setup + Op.JUMPDEST + repeated_code * max_iterations + Op.JUMP(len(setup)) + + self._validate_code_size(code) + + return code + + def _validate_code_size(self, code: Bytecode) -> None: + """Validate that the generated code fits within size limits.""" + if len(code) > self.fork.max_code_size(): + raise ValueError( + f"Generated code size {len(code)} exceeds maximum allowed size " + f"{self.fork.max_code_size()}" + ) + + +class JumpLoopGenerator(BenchmarkCodeGenerator): + """Generates bytecode that loops execution using JUMP operations.""" + + def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction: + """Generate transaction with looping bytecode pattern.""" + # Benchmark Test Structure: + # setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_length) + + code = self.generate_repeated_code(self.attack_block, self.setup) + + return Transaction( + to=pre.deploy_contract(code=code), + gas_limit=self.fork.transaction_gas_limit_cap() or 30_000_000, + sender=pre.fund_eoa(), + ) + + +class ExtCallGenerator(BenchmarkCodeGenerator): + """Generates bytecode that fills the contract to maximum allowed code size.""" + + def generate_transaction(self, pre: Alloc, gas_limit: int) -> Transaction: + """Generate transaction with maximal code size coverage.""" + # 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 + # + # attack = POP(STATICCALL(GAS, target_contract_address, 0, 0, 0, 0)) + # setup + JUMPDEST + attack + attack + ... + attack + JUMP(setup_lengt) + # This could optimize the gas consumption and increase the cycle count. + + max_stack_height = self.fork.max_stack_height() + + target_contract_address = pre.deploy_contract(code=self.attack_block * max_stack_height) + + code_sequence = Op.POP(Op.STATICCALL(Op.GAS, target_contract_address, 0, 0, 0, 0)) + + code = self.generate_repeated_code(code_sequence, Bytecode()) + + return Transaction( + to=pre.deploy_contract(code=code), + gas_limit=self.fork.transaction_gas_limit_cap() or 30_000_000, + sender=pre.fund_eoa(), + ) diff --git a/tests/benchmark/test_worst_compute.py b/tests/benchmark/test_worst_compute.py index 2f706113033..840afef2d50 100644 --- a/tests/benchmark/test_worst_compute.py +++ b/tests/benchmark/test_worst_compute.py @@ -28,6 +28,7 @@ Transaction, add_kzg_version, ) +from ethereum_test_tools.benchmark_code_generator import JumpLoopGenerator from ethereum_test_tools.vm.opcode import Opcodes as Op from ethereum_test_types import TransactionType from ethereum_test_vm.opcode import Opcode @@ -1829,27 +1830,19 @@ def test_worst_jumpis( @pytest.mark.valid_from("Cancun") @pytest.mark.slow def test_worst_jumpdests( - state_test: StateTestFiller, + benchmark_state_test: BenchmarkStateTestFiller, pre: Alloc, + env: Environment, fork: Fork, gas_benchmark_value: int, ): """Test running a JUMPDEST-intensive contract.""" - max_code_size = fork.max_code_size() + generator = JumpLoopGenerator(fork, Op.JUMPDEST) + tx = generator.generate_transaction(pre, gas_benchmark_value) - # 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_state_test( + env=env, + gas_benchmark_value=gas_benchmark_value, pre=pre, post={}, tx=tx, @@ -2780,7 +2773,6 @@ def test_worst_swap( tx = Transaction( to=pre.deploy_contract(code=code), - gas_limit=gas_benchmark_value, sender=pre.fund_eoa(), )