diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d14204450f..95c8b62bee 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -42,6 +42,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Port STATICCALL to CALL tests with zero and non-zero value transfer from `tests/static`, extending coverage with `pytest.mark.with_all_precompiles` ([#1960](https://github.com/ethereum/execution-specs/pull/1960)). - ✨ Add BAL tests that dequeue EIP-7251 consolidation requests. ([#2076](https://github.com/ethereum/execution-specs/pull/2076)). - ✨ Add BAL tests for handling 7702 delegation reset and delegated create. ([#2097](https://github.com/ethereum/execution-specs/pull/2097)). +- ✨ Add benchmark scenarios for ether transfers to precompiles, warm access list transfers, and max-size contract creation transactions ([#2171](https://github.com/ethereum/execution-specs/pull/2171)). ## [v5.4.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v5.4.0) - 2025-12-07 diff --git a/pyproject.toml b/pyproject.toml index 1753805b0a..89e132cf73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -432,8 +432,8 @@ skip = [ "*.csv", "*.tar.gz", "*.png", - "tests/fixtures", - "tests/json_infra/fixtures", + "*/tests/fixtures", + "*/json_infra/fixtures", "fixtures.*", "tmp", "*.coverage*", diff --git a/tests/benchmark/compute/__init__.py b/tests/benchmark/compute/__init__.py new file mode 100644 index 0000000000..99ad82604b --- /dev/null +++ b/tests/benchmark/compute/__init__.py @@ -0,0 +1 @@ +"""Benchmark compute tests package.""" diff --git a/tests/benchmark/compute/instruction/__init__.py b/tests/benchmark/compute/instruction/__init__.py new file mode 100644 index 0000000000..75e6be527f --- /dev/null +++ b/tests/benchmark/compute/instruction/__init__.py @@ -0,0 +1 @@ +"""Benchmark instruction tests package.""" diff --git a/tests/benchmark/compute/instruction/test_account_query.py b/tests/benchmark/compute/instruction/test_account_query.py index c74b2d8db8..8b13e34486 100644 --- a/tests/benchmark/compute/instruction/test_account_query.py +++ b/tests/benchmark/compute/instruction/test_account_query.py @@ -36,7 +36,7 @@ While, ) -from tests.benchmark.compute.helpers import CustomSizedContractFactory +from ..helpers import CustomSizedContractFactory @pytest.mark.repricing(contract_balance=1) diff --git a/tests/benchmark/compute/instruction/test_arithmetic.py b/tests/benchmark/compute/instruction/test_arithmetic.py index c6a7261fe1..e6cb8a9081 100644 --- a/tests/benchmark/compute/instruction/test_arithmetic.py +++ b/tests/benchmark/compute/instruction/test_arithmetic.py @@ -30,7 +30,7 @@ Transaction, ) -from tests.benchmark.compute.helpers import DEFAULT_BINOP_ARGS, make_dup, neg +from ..helpers import DEFAULT_BINOP_ARGS, make_dup, neg @pytest.mark.parametrize( diff --git a/tests/benchmark/compute/instruction/test_bitwise.py b/tests/benchmark/compute/instruction/test_bitwise.py index 462870ff85..d062cdd0a2 100644 --- a/tests/benchmark/compute/instruction/test_bitwise.py +++ b/tests/benchmark/compute/instruction/test_bitwise.py @@ -27,7 +27,7 @@ Transaction, ) -from tests.benchmark.compute.helpers import ( +from ..helpers import ( DEFAULT_BINOP_ARGS, make_dup, sar, diff --git a/tests/benchmark/compute/instruction/test_call_context.py b/tests/benchmark/compute/instruction/test_call_context.py index 8cf5f35950..584c5e21c6 100644 --- a/tests/benchmark/compute/instruction/test_call_context.py +++ b/tests/benchmark/compute/instruction/test_call_context.py @@ -24,7 +24,7 @@ Op, ) -from tests.benchmark.compute.helpers import ( +from ..helpers import ( ReturnDataStyle, ) diff --git a/tests/benchmark/compute/instruction/test_storage.py b/tests/benchmark/compute/instruction/test_storage.py index 68420c284e..44edc54d3e 100644 --- a/tests/benchmark/compute/instruction/test_storage.py +++ b/tests/benchmark/compute/instruction/test_storage.py @@ -29,7 +29,7 @@ compute_create_address, ) -from tests.benchmark.compute.helpers import StorageAction, TransactionResult +from ..helpers import StorageAction, TransactionResult @pytest.mark.repricing(fixed_key=True, fixed_value=True) diff --git a/tests/benchmark/compute/precompile/__init__.py b/tests/benchmark/compute/precompile/__init__.py new file mode 100644 index 0000000000..bf86eb1718 --- /dev/null +++ b/tests/benchmark/compute/precompile/__init__.py @@ -0,0 +1 @@ +"""Benchmark precompile tests package.""" diff --git a/tests/benchmark/compute/precompile/test_alt_bn128.py b/tests/benchmark/compute/precompile/test_alt_bn128.py index 8f1355f9cc..b9b56db8db 100644 --- a/tests/benchmark/compute/precompile/test_alt_bn128.py +++ b/tests/benchmark/compute/precompile/test_alt_bn128.py @@ -13,7 +13,7 @@ ) from py_ecc.bn128 import G1, G2, multiply -from tests.benchmark.compute.helpers import concatenate_parameters +from ..helpers import concatenate_parameters @pytest.mark.parametrize( diff --git a/tests/benchmark/compute/precompile/test_blake2f.py b/tests/benchmark/compute/precompile/test_blake2f.py index 7c2168c38a..db7e7ff2ae 100644 --- a/tests/benchmark/compute/precompile/test_blake2f.py +++ b/tests/benchmark/compute/precompile/test_blake2f.py @@ -9,10 +9,11 @@ Op, ) -from tests.benchmark.compute.helpers import concatenate_parameters from tests.istanbul.eip152_blake2.common import Blake2bInput from tests.istanbul.eip152_blake2.spec import Spec as Blake2bSpec +from ..helpers import concatenate_parameters + @pytest.mark.parametrize( "precompile_address,calldata", diff --git a/tests/benchmark/compute/precompile/test_bls12_381.py b/tests/benchmark/compute/precompile/test_bls12_381.py index 653473dac6..7e59d955d2 100644 --- a/tests/benchmark/compute/precompile/test_bls12_381.py +++ b/tests/benchmark/compute/precompile/test_bls12_381.py @@ -10,9 +10,10 @@ Op, ) -from tests.benchmark.compute.helpers import concatenate_parameters from tests.prague.eip2537_bls_12_381_precompiles import spec as bls12381_spec +from ..helpers import concatenate_parameters + @pytest.mark.parametrize( "precompile_address,calldata", diff --git a/tests/benchmark/compute/precompile/test_ecrecover.py b/tests/benchmark/compute/precompile/test_ecrecover.py index 22aca3b5ca..a3906f721d 100644 --- a/tests/benchmark/compute/precompile/test_ecrecover.py +++ b/tests/benchmark/compute/precompile/test_ecrecover.py @@ -9,7 +9,7 @@ Op, ) -from tests.benchmark.compute.helpers import concatenate_parameters +from ..helpers import concatenate_parameters @pytest.mark.repricing diff --git a/tests/benchmark/compute/precompile/test_identity.py b/tests/benchmark/compute/precompile/test_identity.py index 959e06ccc6..c57ad61be7 100644 --- a/tests/benchmark/compute/precompile/test_identity.py +++ b/tests/benchmark/compute/precompile/test_identity.py @@ -8,7 +8,7 @@ Op, ) -from tests.benchmark.compute.helpers import calculate_optimal_input_length +from ..helpers import calculate_optimal_input_length def test_identity( diff --git a/tests/benchmark/compute/precompile/test_p256verify.py b/tests/benchmark/compute/precompile/test_p256verify.py index 7708382595..514f5989c5 100644 --- a/tests/benchmark/compute/precompile/test_p256verify.py +++ b/tests/benchmark/compute/precompile/test_p256verify.py @@ -9,9 +9,10 @@ Op, ) -from tests.benchmark.compute.helpers import concatenate_parameters from tests.osaka.eip7951_p256verify_precompiles import spec as p256verify_spec +from ..helpers import concatenate_parameters + @pytest.mark.parametrize( "precompile_address,calldata", diff --git a/tests/benchmark/compute/precompile/test_point_evaluation.py b/tests/benchmark/compute/precompile/test_point_evaluation.py index 7597a9fa1d..09e84499fe 100644 --- a/tests/benchmark/compute/precompile/test_point_evaluation.py +++ b/tests/benchmark/compute/precompile/test_point_evaluation.py @@ -9,9 +9,10 @@ Op, ) -from tests.benchmark.compute.helpers import concatenate_parameters from tests.cancun.eip4844_blobs.spec import Spec as BlobsSpec +from ..helpers import concatenate_parameters + @pytest.mark.repricing @pytest.mark.parametrize( diff --git a/tests/benchmark/compute/precompile/test_ripemd160.py b/tests/benchmark/compute/precompile/test_ripemd160.py index 321f7d4a88..e01895f6a4 100644 --- a/tests/benchmark/compute/precompile/test_ripemd160.py +++ b/tests/benchmark/compute/precompile/test_ripemd160.py @@ -8,7 +8,7 @@ Op, ) -from tests.benchmark.compute.helpers import calculate_optimal_input_length +from ..helpers import calculate_optimal_input_length def test_ripemd160( diff --git a/tests/benchmark/compute/precompile/test_sha256.py b/tests/benchmark/compute/precompile/test_sha256.py index e7265a24bd..abcb7389be 100644 --- a/tests/benchmark/compute/precompile/test_sha256.py +++ b/tests/benchmark/compute/precompile/test_sha256.py @@ -8,7 +8,7 @@ Op, ) -from tests.benchmark.compute.helpers import calculate_optimal_input_length +from ..helpers import calculate_optimal_input_length def test_sha256( diff --git a/tests/benchmark/compute/scenario/__init__.py b/tests/benchmark/compute/scenario/__init__.py new file mode 100644 index 0000000000..c295bf2281 --- /dev/null +++ b/tests/benchmark/compute/scenario/__init__.py @@ -0,0 +1 @@ +"""Benchmark scenario tests package.""" diff --git a/tests/benchmark/compute/scenario/test_transaction_types.py b/tests/benchmark/compute/scenario/test_transaction_types.py index 7c72af0b56..7528606dad 100644 --- a/tests/benchmark/compute/scenario/test_transaction_types.py +++ b/tests/benchmark/compute/scenario/test_transaction_types.py @@ -2,7 +2,8 @@ import math import random -from typing import Generator, Tuple +from dataclasses import dataclass +from typing import Generator, List, Tuple import pytest from execution_testing import ( @@ -13,11 +14,11 @@ AuthorizationTuple, BenchmarkTestFiller, Block, - BlockchainTestFiller, Fork, Hash, Op, Transaction, + compute_create_address, ) @@ -31,21 +32,6 @@ def test_empty_block( ) -@pytest.fixture -def iteration_count(intrinsic_cost: int, gas_benchmark_value: int) -> int: - """ - Calculate the number of iterations based on the gas limit and intrinsic - cost. - """ - return gas_benchmark_value // intrinsic_cost - - -@pytest.fixture -def transfer_amount() -> int: - """Ether to transfer in each transaction.""" - return 1 - - @pytest.fixture def intrinsic_cost(fork: Fork) -> int: """Transaction intrinsic cost.""" @@ -60,11 +46,13 @@ def get_distinct_sender_list(pre: Alloc) -> Generator[Address, None, None]: def get_distinct_receiver_list( - pre: Alloc, balance: int + pre: Alloc, + balance: int, + delegation: Address | None = None, ) -> Generator[Address, None, None]: """Get a list of distinct receiver accounts.""" while True: - yield pre.fund_eoa(balance) + yield pre.fund_eoa(balance, delegation=delegation) def get_single_sender_list(pre: Alloc) -> Generator[Address, None, None]: @@ -75,19 +63,38 @@ def get_single_sender_list(pre: Alloc) -> Generator[Address, None, None]: def get_single_receiver_list( - pre: Alloc, balance: int + pre: Alloc, + balance: int, + delegation: Address | None = None, ) -> Generator[Address, None, None]: """Get a list of single receiver accounts.""" - receiver = pre.fund_eoa(balance) + receiver = pre.fund_eoa(balance, delegation=delegation) while True: yield receiver +@dataclass(frozen=True) +class ReceiverAccountType: + """Receiver account type for ether transfer benchmarks.""" + + balance: int + delegated: bool + + @pytest.fixture def ether_transfer_case( - case_id: str, pre: Alloc, balance: int + case_id: str, + pre: Alloc, + receiver_account_type: ReceiverAccountType, ) -> Tuple[Generator[Address, None, None], Generator[Address, None, None]]: """Generate sender and receiver generators based on the test case.""" + balance = receiver_account_type.balance + delegation = ( + pre.deploy_contract(code=Op.STOP) + if receiver_account_type.delegated + else None + ) + if case_id == "a_to_a": """Sending to self.""" senders = get_single_sender_list(pre) @@ -96,22 +103,22 @@ def ether_transfer_case( elif case_id == "a_to_b": """One sender → one receiver.""" senders = get_single_sender_list(pre) - receivers = get_single_receiver_list(pre, balance) + receivers = get_single_receiver_list(pre, balance, delegation) elif case_id == "diff_acc_to_b": """Multiple senders → one receiver.""" senders = get_distinct_sender_list(pre) - receivers = get_single_receiver_list(pre, balance) + receivers = get_single_receiver_list(pre, balance, delegation) elif case_id == "a_to_diff_acc": """One sender → multiple receivers.""" senders = get_single_sender_list(pre) - receivers = get_distinct_receiver_list(pre, balance) + receivers = get_distinct_receiver_list(pre, balance, delegation) elif case_id == "diff_acc_to_diff_acc": """Multiple senders → multiple receivers.""" senders = get_distinct_sender_list(pre) - receivers = get_distinct_receiver_list(pre, balance) + receivers = get_distinct_receiver_list(pre, balance, delegation) else: raise ValueError(f"Unknown case: {case_id}") @@ -129,15 +136,34 @@ def ether_transfer_case( "diff_acc_to_diff_acc", ], ) -@pytest.mark.parametrize("balance", [0, 1]) -def test_block_full_of_ether_transfers( +@pytest.mark.parametrize("transfer_amount", [0, 1]) +@pytest.mark.parametrize( + "receiver_account_type", + [ + pytest.param( + ReceiverAccountType(balance=0, delegated=False), + id="empty_account", + ), + pytest.param( + ReceiverAccountType(balance=1, delegated=False), + id="non_empty_account", + ), + pytest.param( + ReceiverAccountType(balance=0, delegated=True), + id="delegated_account", + ), + ], +) +@pytest.mark.parametrize("warm_access", [False, True]) +def test_ether_transfers( benchmark_test: BenchmarkTestFiller, pre: Alloc, case_id: str, - balance: int, - iteration_count: int, + receiver_account_type: ReceiverAccountType, transfer_amount: int, - intrinsic_cost: int, + fork: Fork, + gas_benchmark_value: int, + warm_access: bool, ether_transfer_case: Tuple[ Generator[Address, None, None], Generator[Address, None, None] ], @@ -151,33 +177,53 @@ def test_block_full_of_ether_transfers( - diff_acc_to_b: multiple senders → one receiver - a_to_diff_acc: one sender → multiple receivers - diff_acc_to_diff_acc: multiple senders → multiple receivers + + When warm_access is True, each transaction includes an access list + entry for the receiver to warm the account before the transfer. """ senders, receivers = ether_transfer_case - # Create a single block with all transactions + balance = receiver_account_type.balance + txs = [] token_transfers: dict[Address, int] = {} + + iteration_cost = fork.transaction_intrinsic_cost_calculator()( + access_list=( + [AccessList(address=Address(0x100), storage_keys=[])] + if warm_access + else None + ), + ) + iteration_count = gas_benchmark_value // iteration_cost + for _ in range(iteration_count): receiver = next(receivers) token_transfers[receiver] = ( token_transfers.get(receiver, 0) + transfer_amount ) + access_list = ( + [AccessList(address=receiver, storage_keys=[])] + if warm_access + else None + ) txs.append( Transaction( to=receiver, value=transfer_amount, - gas_limit=intrinsic_cost, + gas_limit=iteration_cost, sender=next(senders), + access_list=access_list, ) ) - # Only include post state for non a_to_a cases post_state = ( {} if case_id == "a_to_a" else { receiver: Account(balance=balance + transferred_amount) for receiver, transferred_amount in token_transfers.items() + if balance + transferred_amount > 0 } ) @@ -185,6 +231,36 @@ def test_block_full_of_ether_transfers( pre=pre, post=post_state, blocks=[Block(txs=txs)], + expected_benchmark_gas_used=iteration_count * iteration_cost, + ) + + +@pytest.mark.with_all_precompiles +@pytest.mark.parametrize("transfer_amount", [0, 1]) +def test_ether_transfers_to_precompile( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + precompile: int, + gas_benchmark_value: int, + transfer_amount: int, + intrinsic_cost: int, +) -> None: + """Test a block full of ether transfers to a precompile address.""" + iteration_count = gas_benchmark_value // intrinsic_cost + txs = [] + for _ in range(iteration_count): + txs.append( + Transaction( + to=Address(precompile), + value=transfer_amount, + gas_limit=intrinsic_cost, + sender=pre.fund_eoa(), + ) + ) + + benchmark_test( + pre=pre, + blocks=[Block(txs=txs)], expected_benchmark_gas_used=iteration_count * intrinsic_cost, ) @@ -431,44 +507,71 @@ def test_block_full_access_list_and_data( @pytest.mark.parametrize("empty_authority", [True, False]) @pytest.mark.parametrize("zero_delegation", [True, False]) +@pytest.mark.parametrize("empty_account", [True, False]) +@pytest.mark.parametrize("transfer_amount", [True, False]) def test_auth_transaction( - blockchain_test: BlockchainTestFiller, + benchmark_test: BenchmarkTestFiller, pre: Alloc, - intrinsic_cost: int, gas_benchmark_value: int, fork: Fork, empty_authority: bool, + empty_account: bool, + transfer_amount: int, zero_delegation: bool, tx_gas_limit: int, ) -> None: """Test an auth block.""" gas_costs = fork.gas_costs() - - max_auths_per_tx = ( - tx_gas_limit - intrinsic_cost - ) // gas_costs.G_AUTHORIZATION - - total_auth_count = ( - gas_benchmark_value - intrinsic_cost - ) // gas_costs.G_AUTHORIZATION - - num_txs = math.ceil(total_auth_count / max_auths_per_tx) + intrinsic_cost_calc = fork.transaction_intrinsic_cost_calculator() code = Op.INVALID * fork.max_code_size() auth_target = ( Address(0) if zero_delegation else pre.deploy_contract(code=code) ) - txs = [] + remaining_gas = gas_benchmark_value + authorizations_per_tx: List[int] = [] + + min_authorization_intrinsic_gas = intrinsic_cost_calc( + authorization_list_or_count=1 + ) + + while remaining_gas >= min_authorization_intrinsic_gas: + tx_max_gas = min(remaining_gas, tx_gas_limit) + + low = 1 + high = 2 + + # Exponential search to find upper bound + while ( + intrinsic_cost_calc(authorization_list_or_count=high) < tx_max_gas + ): + low = high + high *= 2 + + # Binary search for exact fit + while low < high: + mid = (low + high) // 2 + + if ( + intrinsic_cost_calc(authorization_list_or_count=mid) + > tx_max_gas + ): + high = mid + else: + low = mid + 1 + + best_iterations = low - 1 + authorizations_per_tx.append(best_iterations) + remaining_gas -= intrinsic_cost_calc( + authorization_list_or_count=best_iterations + ) + total_gas_used = 0 total_refund = 0 - remaining_auths = total_auth_count - - for _ in range(num_txs): - # Calculate authorization tuples for this transaction - auths_in_this_tx = min(max_auths_per_tx, remaining_auths) - remaining_auths -= auths_in_this_tx + txs = [] + for auths_in_this_tx in authorizations_per_tx: auth_tuples = [] for _ in range(auths_in_this_tx): signer = ( @@ -481,7 +584,7 @@ def test_auth_transaction( ) auth_tuples.append(auth_tuple) - tx_gas_used = fork.transaction_intrinsic_cost_calculator()( + tx_gas_used = intrinsic_cost_calc( authorization_list_or_count=auth_tuples ) total_gas_used += tx_gas_used @@ -496,18 +599,86 @@ def test_auth_transaction( * auths_in_this_tx, ) + receiver = pre.fund_eoa(0 if empty_account else 1) + txs.append( Transaction( - to=pre.empty_account(), + to=receiver, + value=transfer_amount, gas_limit=tx_gas_used, sender=pre.fund_eoa(), authorization_list=auth_tuples, ) ) - blockchain_test( + benchmark_test( pre=pre, post={}, blocks=[Block(txs=txs)], expected_benchmark_gas_used=total_gas_used - total_refund, ) + + +@pytest.mark.parametrize("transfer_amount", [0, 1]) +@pytest.mark.parametrize( + "contract_size", [0, 1, pytest.param(None, id="contract_size_max")] +) +def test_contract_creation( + benchmark_test: BenchmarkTestFiller, + pre: Alloc, + fork: Fork, + transfer_amount: int, + gas_benchmark_value: int, + contract_size: int | None, +) -> None: + """Benchmark contract creations via transactions.""" + if contract_size is None: + contract_size = fork.max_code_size() + + initcode = Op.RETURN( + Op.PUSH0, + contract_size, + # gas accounting + old_memory_size=0, + new_memory_size=contract_size, + code_deposit_size=contract_size, + ) + intrinsic_gas_calc = fork.transaction_intrinsic_cost_calculator() + + # EIP-7623: actual gas used = max(standard + execution, floor) + standard_intrinsic = intrinsic_gas_calc( + calldata=bytes(initcode), + contract_creation=True, + return_cost_deducted_prior_execution=True, + ) + floor_intrinsic = intrinsic_gas_calc( + calldata=bytes(initcode), + contract_creation=True, + ) + execution_gas = initcode.gas_cost(fork) + tx_cost = max(standard_intrinsic + execution_gas, floor_intrinsic) + + iteration_count = gas_benchmark_value // tx_cost + + sender = pre.fund_eoa() + txs = [] + post = {} + for nonce in range(iteration_count): + txs.append( + Transaction( + to=None, + data=initcode, + value=transfer_amount, + gas_limit=tx_cost, + sender=sender, + ) + ) + created_address = compute_create_address(address=sender, nonce=nonce) + post[created_address] = Account(nonce=1) + + benchmark_test( + pre=pre, + post=post, + blocks=[Block(txs=txs)], + expected_benchmark_gas_used=iteration_count * tx_cost, + ) diff --git a/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py b/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py index 9ac2e77245..21441a73b9 100644 --- a/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py +++ b/tests/benchmark/compute/scenario/test_unchunkified_bytecode.py @@ -19,7 +19,7 @@ While, ) -from tests.benchmark.compute.helpers import CustomSizedContractFactory +from ..helpers import CustomSizedContractFactory @pytest.mark.parametrize(