diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 79b750fcd2c..3544c95ebd7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -114,6 +114,7 @@ Users can select any of the artifacts depending on their testing needs for their - ✨ Opcode classes now validate keyword arguments and raise `ValueError` with clear error messages ([#1739](https://github.com/ethereum/execution-spec-tests/pull/1739), [#1856](https://github.com/ethereum/execution-spec-tests/pull/1856)). - ✨ All commands (`fill`, `consume`, `execute`) now work without having to clone the repository, e.g. `uv run --with git+https://github.com/ethereum/execution-spec-tests.git consume` now works from any folder ([#1863](https://github.com/ethereum/execution-spec-tests/pull/1863)). - 🔀 Move Prague to stable and Osaka to develop ([#1573](https://github.com/ethereum/execution-spec-tests/pull/1573)). +- ✨ Add a `pytest.mark.with_all_typed_transactions` marker that creates default typed transactions for each `tx_type` supported by the current `fork` ([#1890](https://github.com/ethereum/execution-spec-tests/pull/1890)). ### 🧪 Test Cases @@ -128,6 +129,7 @@ Users can select any of the artifacts depending on their testing needs for their - ✨ [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934): Add test cases for the block RLP max limit of 10MiB ([#1730](https://github.com/ethereum/execution-spec-tests/pull/1730)). - ✨ [EIP-7939](https://eips.ethereum.org/EIPS/eip-7939) Add count leading zeros (CLZ) opcode tests for Osaka ([#1733](https://github.com/ethereum/execution-spec-tests/pull/1733)). - ✨ [EIP-7918](https://eips.ethereum.org/EIPS/eip-7918): Blob base fee bounded by execution cost test cases (initial), includes some adjustments to [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) tests ([#1685](https://github.com/ethereum/execution-spec-tests/pull/1685)). Update the blob_base_cost ([#1915](https://github.com/ethereum/EIPs/pull/1915)). +- ✨ [EIP-7934](https://eips.ethereum.org/EIPS/eip-7934): Add additional test cases for block RLP max limit with all typed transactions and for a log-creating transactions ([#1890](https://github.com/ethereum/execution-spec-tests/pull/1890)). ## [v4.5.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.5.0) - 2025-05-14 diff --git a/docs/writing_tests/test_markers.md b/docs/writing_tests/test_markers.md index 97a04948adb..48597713013 100644 --- a/docs/writing_tests/test_markers.md +++ b/docs/writing_tests/test_markers.md @@ -55,6 +55,59 @@ This marker is used to automatically parameterize a test with all contract creat This marker only differs from `pytest.mark.with_all_tx_types` in that it does not include transaction type 3 (Blob Transaction type) on fork Cancun and after. +### `@pytest.mark.with_all_typed_transactions` + +This marker is used to automatically parameterize a test with all typed transactions, including `type=0` (legacy transaction), that are valid for the fork being tested. +This marker is an indirect marker that utilizes the `tx_type` values from the `pytest.mark.with_all_tx_types` marker to build default typed transactions for each `tx_type`. + +Optional: Default typed transactions used as values for `typed_transaction` exist in `src/pytest_plugins/shared/transaction_fixtures.py` and can be overridden for the scope of +the test by re-defining the appropriate `pytest.fixture` for that transaction type. + +```python +import pytest + +from ethereum_test_tools import Account, Alloc, StateTestFiller +from ethereum_test_types import Transaction + +# Optional override for type 2 transaction +@pytest.fixture +def type_2_default_transaction(sender: Account): + return Transaction( + ty=2, + sender=sender, + max_fee_per_gas=0x1337, + max_priority_fee_per_gas=0x1337, + ... + ) + +# Optional override for type 4 transaction +@pytest.fixture +def type_4_default_transaction(sender: Account, pre: Alloc): + return Transaction( + ty=4, + sender=sender, + ..., + authorization_list=[ + AuthorizationTuple( + address=Address(1234), + nonce=0, + chain_id=1, + signer=pre.fund_eoa(), + ) + ] + ) + + +@pytest.mark.with_all_typed_transactions +@pytest.mark.valid_from("Prague") +def test_something_with_all_tx_types( + state_test: StateTestFiller, + pre: Alloc, + typed_transaction: Transaction +): + pass +``` + ### `@pytest.mark.with_all_precompiles` This marker is used to automatically parameterize a test with all precompiles that are valid for the fork being tested. diff --git a/src/cli/pytest_commands/pytest_ini_files/pytest-check-eip-versions.ini b/src/cli/pytest_commands/pytest_ini_files/pytest-check-eip-versions.ini index b8263df6022..aad7413ca54 100644 --- a/src/cli/pytest_commands/pytest_ini_files/pytest-check-eip-versions.ini +++ b/src/cli/pytest_commands/pytest_ini_files/pytest-check-eip-versions.ini @@ -13,6 +13,7 @@ addopts = -p pytest_plugins.filler.pre_alloc -p pytest_plugins.filler.filler -p pytest_plugins.shared.execute_fill + -p pytest_plugins.shared.transaction_fixtures -p pytest_plugins.forks.forks -p pytest_plugins.help.help -m eip_version_check diff --git a/src/cli/pytest_commands/pytest_ini_files/pytest-execute-hive.ini b/src/cli/pytest_commands/pytest_ini_files/pytest-execute-hive.ini index ef05601d13a..a575d2d7372 100644 --- a/src/cli/pytest_commands/pytest_ini_files/pytest-execute-hive.ini +++ b/src/cli/pytest_commands/pytest_ini_files/pytest-execute-hive.ini @@ -14,6 +14,7 @@ addopts = -p pytest_plugins.execute.rpc.hive -p pytest_plugins.execute.execute -p pytest_plugins.shared.execute_fill + -p pytest_plugins.shared.transaction_fixtures -p pytest_plugins.forks.forks -p pytest_plugins.pytest_hive.pytest_hive -p pytest_plugins.help.help diff --git a/src/cli/pytest_commands/pytest_ini_files/pytest-execute.ini b/src/cli/pytest_commands/pytest_ini_files/pytest-execute.ini index 3ec70273ca3..9e7579247a9 100644 --- a/src/cli/pytest_commands/pytest_ini_files/pytest-execute.ini +++ b/src/cli/pytest_commands/pytest_ini_files/pytest-execute.ini @@ -14,6 +14,7 @@ addopts = -p pytest_plugins.execute.pre_alloc -p pytest_plugins.execute.execute -p pytest_plugins.shared.execute_fill + -p pytest_plugins.shared.transaction_fixtures -p pytest_plugins.execute.rpc.remote_seed_sender -p pytest_plugins.execute.rpc.remote -p pytest_plugins.forks.forks diff --git a/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini b/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini index ee6147a5c15..5c768709765 100644 --- a/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini +++ b/src/cli/pytest_commands/pytest_ini_files/pytest-fill.ini @@ -15,6 +15,7 @@ addopts = -p pytest_plugins.filler.ported_tests -p pytest_plugins.filler.static_filler -p pytest_plugins.shared.execute_fill + -p pytest_plugins.shared.transaction_fixtures -p pytest_plugins.forks.forks -p pytest_plugins.eels_resolver -p pytest_plugins.help.help diff --git a/src/pytest_plugins/forks/forks.py b/src/pytest_plugins/forks/forks.py index ed1e27e4e59..203960a073d 100644 --- a/src/pytest_plugins/forks/forks.py +++ b/src/pytest_plugins/forks/forks.py @@ -257,6 +257,8 @@ class CovariantDecorator(CovariantDescriptor): description: Description of the marker. fork_attribute_name: Name of the method to call on the fork to get the values. marker_parameter_names: Names of the parameters to be parametrized in the test function. + indirect: Whether the parameters should be passed through fixtures (indirect + parametrization). """ @@ -264,6 +266,7 @@ class CovariantDecorator(CovariantDescriptor): description: ClassVar[str] fork_attribute_name: ClassVar[str] marker_parameter_names: ClassVar[List[str]] + indirect: ClassVar[bool] def __init__(self, metafunc: Metafunc): """ @@ -317,6 +320,7 @@ def covariant_decorator( description: str, fork_attribute_name: str, argnames: List[str], + indirect: bool = False, ) -> Type[CovariantDecorator]: """Generate a new covariant decorator subclass.""" return type( @@ -327,6 +331,7 @@ def covariant_decorator( "description": description, "fork_attribute_name": fork_attribute_name, "marker_parameter_names": argnames, + "indirect": indirect, }, ) @@ -346,6 +351,16 @@ def covariant_decorator( fork_attribute_name="contract_creating_tx_types", argnames=["tx_type"], ), + covariant_decorator( + marker_name="with_all_typed_transactions", + description="marks a test to be parametrized with default typed transactions named " + "typed_transaction", + fork_attribute_name="tx_types", + argnames=["typed_transaction"], + # indirect means the values from `tx_types` will be passed to the + # `typed_transaction` fixture which will then be used in the test + indirect=True, + ), covariant_decorator( marker_name="with_all_precompiles", description="marks a test to be parametrized for all precompiles at parameter named" @@ -972,10 +987,15 @@ def add_fork_covariant_parameters( metafunc: Metafunc, fork_parametrizers: List[ForkParametrizer] ) -> None: """Iterate over the fork covariant descriptors and add their values to the test function.""" + # Process all covariant decorators uniformly for covariant_descriptor in fork_covariant_decorators: - for fork_parametrizer in fork_parametrizers: - covariant_descriptor(metafunc=metafunc).add_values(fork_parametrizer=fork_parametrizer) + if list(metafunc.definition.iter_markers(covariant_descriptor.marker_name)): + for fork_parametrizer in fork_parametrizers: + covariant_descriptor(metafunc=metafunc).add_values( + fork_parametrizer=fork_parametrizer + ) + # Handle custom parametrize_by_fork markers for marker in metafunc.definition.iter_markers(): if marker.name == "parametrize_by_fork": descriptor = CovariantDescriptor( @@ -1029,6 +1049,16 @@ def parameters_from_fork_parametrizer_list( def parametrize_fork(metafunc: Metafunc, fork_parametrizers: List[ForkParametrizer]) -> None: """Add the fork parameters to the test function.""" - metafunc.parametrize( - *parameters_from_fork_parametrizer_list(fork_parametrizers), scope="function" - ) + param_names, param_values = parameters_from_fork_parametrizer_list(fork_parametrizers) + + # Collect all parameters that should be indirect from the decorators + indirect = [] + for covariant_descriptor in fork_covariant_decorators: + if ( + list(metafunc.definition.iter_markers(covariant_descriptor.marker_name)) + and covariant_descriptor.indirect + ): + # Add all argnames from this decorator to indirect list + indirect.extend(covariant_descriptor.marker_parameter_names) + + metafunc.parametrize(param_names, param_values, scope="function", indirect=indirect) diff --git a/src/pytest_plugins/forks/tests/test_covariant_markers.py b/src/pytest_plugins/forks/tests/test_covariant_markers.py index 745ec261456..adce172eaef 100644 --- a/src/pytest_plugins/forks/tests/test_covariant_markers.py +++ b/src/pytest_plugins/forks/tests/test_covariant_markers.py @@ -262,6 +262,79 @@ def test_case(state_test, system_contract): None, id="with_all_system_contracts", ), + pytest.param( + """ + import pytest + from ethereum_test_tools import Transaction + @pytest.mark.with_all_typed_transactions + @pytest.mark.valid_from("Berlin") + @pytest.mark.valid_until("Berlin") + @pytest.mark.state_test_only + def test_case(state_test, typed_transaction): + assert isinstance(typed_transaction, Transaction) + assert typed_transaction.ty in [0, 1] # Berlin supports types 0 and 1 + """, + {"passed": 2, "failed": 0, "skipped": 0, "errors": 0}, + None, + id="with_all_typed_transactions_berlin", + ), + pytest.param( + """ + import pytest + from ethereum_test_tools import Transaction + @pytest.mark.with_all_typed_transactions() + @pytest.mark.valid_from("London") + @pytest.mark.valid_until("London") + @pytest.mark.state_test_only + def test_case(state_test, typed_transaction, pre): + assert isinstance(typed_transaction, Transaction) + assert typed_transaction.ty in [0, 1, 2] # London supports types 0, 1, 2 + """, + {"passed": 3, "failed": 0, "skipped": 0, "errors": 0}, + None, + id="with_all_typed_transactions_london", + ), + pytest.param( + """ + import pytest + from ethereum_test_tools import Transaction + from ethereum_test_base_types import AccessList + + # Override the type 3 transaction fixture + @pytest.fixture + def type_3_default_transaction(pre): + sender = pre.fund_eoa() + + return Transaction( + ty=3, + sender=sender, + max_fee_per_gas=10**10, + max_priority_fee_per_gas=10**9, + max_fee_per_blob_gas=10**8, + gas_limit=300_000, + data=b"\\xFF" * 50, + access_list=[ + AccessList(address=0x1111, storage_keys=[10, 20]), + ], + blob_versioned_hashes=[ + 0x0111111111111111111111111111111111111111111111111111111111111111, + ], + ) + + @pytest.mark.with_all_typed_transactions() + @pytest.mark.valid_at("Cancun") + @pytest.mark.state_test_only + def test_case(state_test, typed_transaction, pre): + assert isinstance(typed_transaction, Transaction) + if typed_transaction.ty == 3: + # Verify our override worked + assert typed_transaction.data == b"\\xFF" * 50 + assert len(typed_transaction.blob_versioned_hashes) == 1 + """, + {"passed": 4, "failed": 0, "skipped": 0, "errors": 0}, + None, + id="with_all_typed_transactions_with_override", + ), pytest.param( """ import pytest diff --git a/src/pytest_plugins/shared/execute_fill.py b/src/pytest_plugins/shared/execute_fill.py index be14c5d0359..f5e19884323 100644 --- a/src/pytest_plugins/shared/execute_fill.py +++ b/src/pytest_plugins/shared/execute_fill.py @@ -7,6 +7,7 @@ from ethereum_test_execution import BaseExecute, LabeledExecuteFormat from ethereum_test_fixtures import BaseFixture, LabeledFixtureFormat from ethereum_test_specs import BaseTest +from ethereum_test_types import EOA, Alloc from pytest_plugins.spec_version_checker.spec_version_checker import EIPSpecTestItem @@ -150,6 +151,13 @@ def __init__(self, message): ) +# Global `sender` fixture that can be overridden by tests. +@pytest.fixture +def sender(pre: Alloc) -> EOA: + """Fund an EOA from pre-alloc.""" + return pre.fund_eoa() + + def pytest_addoption(parser: pytest.Parser): """Add command-line options to pytest.""" static_filler_group = parser.getgroup("static", "Arguments defining static filler behavior") diff --git a/src/pytest_plugins/shared/transaction_fixtures.py b/src/pytest_plugins/shared/transaction_fixtures.py new file mode 100644 index 00000000000..24f80bd8a50 --- /dev/null +++ b/src/pytest_plugins/shared/transaction_fixtures.py @@ -0,0 +1,160 @@ +""" +Pytest plugin providing default transaction fixtures for each transaction type. + +Each fixture can be overridden in test files to customize transaction behavior. +""" + +import pytest + +from ethereum_test_base_types import AccessList +from ethereum_test_tools import Opcodes as Op +from ethereum_test_types import AuthorizationTuple, Transaction, add_kzg_version + + +@pytest.fixture +def type_0_default_transaction(sender): + """Type 0 (legacy) default transaction available in all forks.""" + return Transaction( + ty=0, + sender=sender, + gas_price=10**9, + gas_limit=100_000, + data=b"\x00" * 100, + ) + + +@pytest.fixture +def type_1_default_transaction(sender): + """Type 1 (access list) default transaction introduced in Berlin fork.""" + return Transaction( + ty=1, + sender=sender, + gas_price=10**9, + gas_limit=100_000, + data=b"\x00" * 100, + access_list=[ + AccessList(address=0x1234, storage_keys=[0, 1, 2]), + AccessList(address=0x5678, storage_keys=[3, 4, 5]), + AccessList(address=0x9ABC, storage_keys=[]), + ], + ) + + +@pytest.fixture +def type_2_default_transaction(sender): + """Type 2 (dynamic fee) default transaction introduced in London fork.""" + return Transaction( + ty=2, + sender=sender, + max_fee_per_gas=10**10, + max_priority_fee_per_gas=10**9, + gas_limit=100_000, + data=b"\x00" * 200, + access_list=[ + AccessList(address=0x2468, storage_keys=[10, 20, 30]), + AccessList(address=0xACE0, storage_keys=[40, 50]), + ], + ) + + +@pytest.fixture +def type_3_default_transaction(sender): + """Type 3 (blob) default transaction introduced in Cancun fork.""" + return Transaction( + ty=3, + sender=sender, + max_fee_per_gas=10**10, + max_priority_fee_per_gas=10**9, + max_fee_per_blob_gas=10**9, + gas_limit=100_000, + data=b"\x00" * 150, + access_list=[ + AccessList(address=0x3690, storage_keys=[100, 200]), + AccessList(address=0xBEEF, storage_keys=[300]), + ], + blob_versioned_hashes=add_kzg_version( + [ + 0x1111111111111111111111111111111111111111111111111111111111111111, + 0x2222222222222222222222222222222222222222222222222222222222222222, + ], + 0x01, + ), + ) + + +@pytest.fixture +def type_4_default_transaction(sender, pre): + """Type 4 (set code) default transaction introduced in Prague fork.""" + # Create authorized accounts with funds + auth_signer1 = pre.fund_eoa(amount=10**18) + auth_signer2 = pre.fund_eoa(amount=10**18) + + # Create target addresses that will be authorized + target1 = pre.deploy_contract(Op.SSTORE(0, 1)) + target2 = pre.deploy_contract(Op.SSTORE(0, 1)) + + return Transaction( + ty=4, + sender=sender, + max_fee_per_gas=10**10, + max_priority_fee_per_gas=10**9, + gas_limit=150_000, + data=b"\x00" * 200, + access_list=[ + AccessList(address=0x4567, storage_keys=[1000, 2000, 3000]), + AccessList(address=0xCDEF, storage_keys=[4000, 5000]), + ], + authorization_list=[ + AuthorizationTuple( + chain_id=1, + address=target1, + nonce=0, + signer=auth_signer1, + ), + AuthorizationTuple( + chain_id=1, + address=target2, + nonce=0, + signer=auth_signer2, + ), + ], + ) + + +@pytest.fixture +def typed_transaction(request, fork): + """ + Fixture that provides a Transaction object based on the parametrized tx type. + + This fixture works with the @pytest.mark.with_all_typed_transactions marker, + which parametrizes the test with all transaction types supported by the fork. + + The actual transaction type value comes from the marker's parametrization. + """ + # The marker parametrizes 'typed_transaction' with tx type integers + # Get the parametrized tx_type value + if hasattr(request, "param"): + # When parametrized by the marker, request.param contains the tx type + tx_type = request.param + else: + raise ValueError( + "`typed_transaction` fixture must be used with " + "`@pytest.mark.with_all_typed_transactions` marker" + ) + + fixture_name = f"type_{tx_type}_default_transaction" + + # Check if fixture exists - try to get it first + try: + # This will find fixtures defined in the test file or plugin + return request.getfixturevalue(fixture_name) + except pytest.FixtureLookupError as e: + # Get all supported tx types for better error message + supported_types = fork.tx_types() + raise NotImplementedError( + f"Fork {fork} supports transaction type {tx_type} but " + f"fixture '{fixture_name}' is not implemented!\n" + f"Fork {fork} supports transaction types: {supported_types}\n" + f"Please add the missing fixture to " + f"src/pytest_plugins/shared/transaction_fixtures.py" + ) from e diff --git a/tests/cancun/eip4844_blobs/test_excess_blob_gas_fork_transition.py b/tests/cancun/eip4844_blobs/test_excess_blob_gas_fork_transition.py index 1f13736814d..0799b969747 100644 --- a/tests/cancun/eip4844_blobs/test_excess_blob_gas_fork_transition.py +++ b/tests/cancun/eip4844_blobs/test_excess_blob_gas_fork_transition.py @@ -47,12 +47,6 @@ def pre_fork_blobs_per_block(fork: Fork) -> int: return 0 -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Sender account.""" - return pre.fund_eoa() - - @pytest.fixture def pre_fork_blocks( pre_fork_blobs_per_block: int, diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index a58b4ac3857..cf218a5dea2 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -189,12 +189,6 @@ def precompile_caller_address( ) -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Return sender account.""" - return pre.fund_eoa() - - @pytest.fixture def tx( precompile_caller_address: Address, diff --git a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py index d5894ccb1ff..639d007542a 100644 --- a/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py +++ b/tests/cancun/eip5656_mcopy/test_mcopy_memory_expansion.py @@ -140,11 +140,6 @@ def caller_address(pre: Alloc, callee_bytecode: bytes) -> Address: # noqa: D103 return pre.deploy_contract(code=callee_bytecode) -@pytest.fixture -def sender(pre: Alloc) -> Address: # noqa: D103 - return pre.fund_eoa() - - @pytest.fixture def tx( # noqa: D103 sender: Address, diff --git a/tests/cancun/eip6780_selfdestruct/conftest.py b/tests/cancun/eip6780_selfdestruct/conftest.py index a4edad5b6f1..0bdc54847e0 100644 --- a/tests/cancun/eip6780_selfdestruct/conftest.py +++ b/tests/cancun/eip6780_selfdestruct/conftest.py @@ -2,13 +2,7 @@ import pytest -from ethereum_test_tools import EOA, Address, Alloc, Environment - - -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """EOA that will be used to send transactions.""" - return pre.fund_eoa() +from ethereum_test_tools import Address, Alloc, Environment @pytest.fixture diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py index d5cc682c87a..c38220fe005 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py @@ -125,12 +125,6 @@ def selfdestruct_code( return selfdestruct_code_preset(sendall_recipient_addresses=sendall_recipient_addresses) -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """EOA that will be used to send transactions.""" - return pre.fund_eoa() - - @pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2]) @pytest.mark.parametrize( "call_times,sendall_recipient_addresses", diff --git a/tests/osaka/eip7934_block_rlp_limit/conftest.py b/tests/osaka/eip7934_block_rlp_limit/conftest.py index 3856d238e8c..651ae98488c 100644 --- a/tests/osaka/eip7934_block_rlp_limit/conftest.py +++ b/tests/osaka/eip7934_block_rlp_limit/conftest.py @@ -3,7 +3,6 @@ import pytest from ethereum_test_tools import ( - EOA, Address, Alloc, ) @@ -23,12 +22,6 @@ def env() -> Environment: return Environment(gas_limit=100_000_000) -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Funded EOA fixture used for sending transactions.""" - return pre.fund_eoa() - - @pytest.fixture def contract_recipient(pre: Alloc) -> Address: """Deploy a simple contract that can receive large calldata.""" diff --git a/tests/osaka/eip7934_block_rlp_limit/spec.py b/tests/osaka/eip7934_block_rlp_limit/spec.py index 335c5d046a9..3f653d1ff54 100644 --- a/tests/osaka/eip7934_block_rlp_limit/spec.py +++ b/tests/osaka/eip7934_block_rlp_limit/spec.py @@ -24,6 +24,7 @@ class Spec: MAX_BLOCK_SIZE = 10_485_760 # 10 MiB SAFETY_MARGIN = 2_097_152 # 2 MiB MAX_RLP_BLOCK_SIZE = MAX_BLOCK_SIZE - SAFETY_MARGIN # 8_388_608 bytes + BLOB_COMMITMENT_VERSION_KZG = 1 @staticmethod def exceed_max_rlp_block_size(rlp_encoded_block: bytes) -> bool: diff --git a/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py b/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py index a30d0d1dab6..5983950c97f 100644 --- a/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py +++ b/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py @@ -8,7 +8,7 @@ import pytest -from ethereum_test_base_types import ZeroPaddedHexNumber +from ethereum_test_base_types import Address, HexNumber, ZeroPaddedHexNumber from ethereum_test_fixtures.blockchain import ( FixtureBlockBase, FixtureHeader, @@ -22,6 +22,7 @@ Bytes, Transaction, ) +from ethereum_test_tools import Opcodes as Op from ethereum_test_types import EOA, Environment from .spec import Spec, ref_spec_7934 @@ -88,53 +89,136 @@ def get_block_rlp_size(transactions: List[Transaction], gas_used: int) -> int: header = create_test_header(gas_used) total_gas = sum((tx.gas_limit or 21000) for tx in transactions) header.gas_used = ZeroPaddedHexNumber(total_gas) + + # Calculate blob gas used if there are blob transactions + blob_gas_used = 0 + for tx in transactions: + if hasattr(tx, "blob_versioned_hashes") and tx.blob_versioned_hashes: + blob_gas_used += len(tx.blob_versioned_hashes) * (2**17) + + if blob_gas_used > 0: + header.blob_gas_used = ZeroPaddedHexNumber(blob_gas_used) + test_block = FixtureBlockBase(blockHeader=header, withdrawals=[]) # type: ignore return len(test_block.with_rlp(txs=transactions).rlp) -@pytest.fixture def exact_size_transactions( - sender: EOA, block_size_limit: int, fork: Fork + sender: EOA, + block_size_limit: int, + fork: Fork, + pre: Alloc, + emit_logs: bool = False, + specific_transaction_to_include: Transaction | None = None, ) -> Tuple[List[Transaction], int]: """ Generate transactions that fill a block to exactly the RLP size limit. The calculation uses caching to avoid recalculating the same block rlp for each fork. Calculate the block and fill with real sender for testing. + + Args: + sender: The sender account + block_size_limit: The target block RLP size limit + fork: The fork to generate transactions for + pre: Required if emit_logs is True, used to deploy the log contract + emit_logs: If True, transactions will call a contract that emits logs + specific_transaction_to_include: If provided, this transaction will be included + """ - stubbed_transactions, gas_used = _exact_size_transactions_calculation(block_size_limit, fork) - test_transactions = [ - Transaction( - sender=sender, - nonce=tx.nonce, - max_fee_per_gas=tx.max_fee_per_gas, - max_priority_fee_per_gas=tx.max_priority_fee_per_gas, - gas_limit=tx.gas_limit, - data=tx.data, + log_contract = None + if emit_logs: + if pre is None: + raise ValueError("pre is required when emit_logs is True") + # Deploy a contract that emits logs + log_contract_code = Op.SSTORE(1, 1) + # Emit multiple LOG4 events with maximum data and topics + for _ in range(3): + log_contract_code += Op.PUSH32( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + ) # topic 4 + log_contract_code += Op.PUSH32( + 0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE + ) # topic 3 + log_contract_code += Op.PUSH32( + 0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD + ) # topic 2 + log_contract_code += Op.PUSH32( + 0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC + ) # topic 1 + log_contract_code += Op.PUSH1(32) # size + log_contract_code += Op.PUSH1(0) # offset + log_contract_code += Op.LOG4 + log_contract = pre.deploy_contract(log_contract_code) + + if not specific_transaction_to_include: + # use cached version when possible for performance + stubbed_transactions, gas_used = _exact_size_transactions_cached( + block_size_limit, fork, emit_logs_contract=log_contract ) - for tx in stubbed_transactions - ] + else: + # Direct calculation, no cache, since `Transaction` is not hashable + stubbed_transactions, gas_used = _exact_size_transactions_impl( + block_size_limit, + fork, + specific_transaction_to_include=specific_transaction_to_include, + ) + + test_transactions = [] + for tx in stubbed_transactions: + # Create a new transaction with the correct sender, preserving all other fields + tx_dict = tx.model_dump(exclude_unset=True) + tx_dict.pop("r", None) + tx_dict.pop("s", None) + tx_dict.pop("v", None) + tx_dict["sender"] = sender + test_transactions.append(Transaction(**tx_dict)) return test_transactions, gas_used @lru_cache(maxsize=128) -def _exact_size_transactions_calculation( - block_size_limit: int, fork: Fork +def _exact_size_transactions_cached( + block_size_limit: int, + fork: Fork, + emit_logs_contract: Address | None = None, +) -> Tuple[List[Transaction], int]: + """ + Generate transactions that fill a block to exactly the RLP size limit. Abstracted + with hashable arguments for caching block calculations. + """ + return _exact_size_transactions_impl(block_size_limit, fork, None, emit_logs_contract) + + +def _exact_size_transactions_impl( + block_size_limit: int, + fork: Fork, + specific_transaction_to_include: Transaction | None = None, + emit_logs_contract: Address | None = None, ) -> Tuple[List[Transaction], int]: - """Generate transactions that fill a block to exactly the RLP size limit.""" + """ + Calculate the exact size of transactions to be included. Shared by both cached and + non-cached paths. + """ transactions = [] - sender = EOA("0x" + "00" * 20, key=123) # stub account to fill the block + sender = EOA("0x" + "00" * 20, key=123) nonce = 0 total_gas_used = 0 max_block_gas = 100_000_000 calculator = fork.transaction_intrinsic_cost_calculator() - data_large = b"\x00" * 500_000 + data_large = Bytes(b"\x00" * 500_000) gas_limit_large = calculator(calldata=data_large) # block with 16 transactions + large calldata remains safely below the limit - for _ in range(16): + # add 15 generic transactions to fill the block and one typed transaction + # if tx_type is specified, otherwise just add 16 generic transactions + not_all_generic_txs = any( + kwarg is not None for kwarg in [specific_transaction_to_include, emit_logs_contract] + ) + + generic_tx_num = 15 if not_all_generic_txs else 16 + for _ in range(generic_tx_num): tx = Transaction( sender=sender, nonce=nonce, @@ -143,11 +227,45 @@ def _exact_size_transactions_calculation( gas_limit=gas_limit_large, data=data_large, ) - transactions.append(tx) total_gas_used += gas_limit_large nonce += 1 + # append a typed transaction to fill the block + if not_all_generic_txs: + if specific_transaction_to_include is not None: + tx_dict = specific_transaction_to_include.model_dump(exclude_unset=True) + data = Bytes(b"\x00" * 200_000) + gas_limit = HexNumber( + calculator( + calldata=data, + access_list=specific_transaction_to_include.access_list, + authorization_list_or_count=len(tx_dict.get("authorization_list", [])), + ) + ) + tx_dict["sender"] = sender + tx_dict["nonce"] = nonce + tx_dict["data"] = data + tx_dict["gas_limit"] = gas_limit + last_tx = Transaction(**tx_dict) + elif emit_logs_contract is not None: + last_tx = Transaction( + sender=sender, + nonce=nonce, + max_fee_per_gas=10**11, + max_priority_fee_per_gas=10**11, + gas_limit=calculator(calldata=b""), + to=emit_logs_contract, + ) + else: + raise ValueError( + "Either specific_transaction_to_include or emit_logs_contract must be provided." + ) + + transactions.append(last_tx) + nonce += 1 + total_gas_used += last_tx.gas_limit + current_size = get_block_rlp_size(transactions, gas_used=total_gas_used) remaining_bytes = block_size_limit - current_size remaining_gas = max_block_gas - total_gas_used @@ -259,9 +377,10 @@ def test_block_at_rlp_size_limit_boundary( blockchain_test: BlockchainTestFiller, pre: Alloc, post: Alloc, - block_size_limit: int, env: Environment, - exact_size_transactions, + sender: EOA, + fork: Fork, + block_size_limit: int, delta: int, ): """ @@ -271,7 +390,12 @@ def test_block_at_rlp_size_limit_boundary( - At the limit, the block is valid - At the limit + 1 byte, the block is invalid """ - transactions, gas_used = exact_size_transactions + transactions, gas_used = exact_size_transactions( + sender, + block_size_limit, + fork, + pre, + ) block_rlp_size = get_block_rlp_size(transactions, gas_used=gas_used) assert block_rlp_size == block_size_limit, ( f"Block RLP size {block_rlp_size} does not exactly match limit {block_size_limit}, " @@ -298,3 +422,78 @@ def test_block_at_rlp_size_limit_boundary( blocks=[block], verify_sync=False if delta > 0 else True, ) + + +@pytest.mark.with_all_typed_transactions +def test_block_rlp_size_at_limit_with_all_typed_transactions( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + post: Alloc, + fork: Fork, + sender: EOA, + block_size_limit: int, + env: Environment, + typed_transaction: Transaction, +) -> None: + """Test the block RLP size limit with all transaction types.""" + transactions, gas_used = exact_size_transactions( + sender, + block_size_limit, + fork, + pre, + specific_transaction_to_include=typed_transaction, + ) + block_rlp_size = get_block_rlp_size(transactions, gas_used=gas_used) + assert block_rlp_size == block_size_limit, ( + f"Block RLP size {block_rlp_size} does not exactly match limit {block_size_limit}, " + f"difference: {block_rlp_size - block_size_limit} bytes" + ) + + block = Block(txs=transactions) + block.extra_data = Bytes(EXTRA_DATA_AT_LIMIT) + block.timestamp = ZeroPaddedHexNumber(HEADER_TIMESTAMP) + + blockchain_test( + genesis_environment=env, + pre=pre, + post=post, + blocks=[block], + verify_sync=True, + ) + + +def test_block_at_rlp_limit_with_logs( + blockchain_test: BlockchainTestFiller, + pre: Alloc, + post: Alloc, + env: Environment, + sender: EOA, + fork: Fork, + block_size_limit: int, +): + """Test that a block at the RLP size limit is valid even when transactions emit logs.""" + transactions, gas_used = exact_size_transactions( + sender, + block_size_limit, + fork, + pre, + emit_logs=True, + ) + + block_rlp_size = get_block_rlp_size(transactions, gas_used=gas_used) + assert block_rlp_size == block_size_limit, ( + f"Block RLP size {block_rlp_size} does not exactly match limit {block_size_limit}, " + f"difference: {block_rlp_size - block_size_limit} bytes" + ) + + block = Block(txs=transactions) + block.extra_data = Bytes(EXTRA_DATA_AT_LIMIT) + block.timestamp = ZeroPaddedHexNumber(HEADER_TIMESTAMP) + + blockchain_test( + genesis_environment=env, + pre=pre, + post=post, + blocks=[block], + verify_sync=True, + ) diff --git a/tests/osaka/eip7951_p256verify_precompiles/conftest.py b/tests/osaka/eip7951_p256verify_precompiles/conftest.py index fc81df4be9b..17833d4408f 100644 --- a/tests/osaka/eip7951_p256verify_precompiles/conftest.py +++ b/tests/osaka/eip7951_p256verify_precompiles/conftest.py @@ -118,12 +118,6 @@ def call_contract_address(pre: Alloc, call_contract_code: Bytecode) -> Address: return pre.deploy_contract(call_contract_code) -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Sender of the transaction.""" - return pre.fund_eoa() - - @pytest.fixture def post(call_contract_address: Address, call_contract_post_storage: Storage): """Test expected post outcome.""" diff --git a/tests/prague/eip7623_increase_calldata_cost/conftest.py b/tests/prague/eip7623_increase_calldata_cost/conftest.py index 5b85a6bff79..5f0a4b50d0d 100644 --- a/tests/prague/eip7623_increase_calldata_cost/conftest.py +++ b/tests/prague/eip7623_increase_calldata_cost/conftest.py @@ -24,12 +24,6 @@ from .helpers import DataTestType, find_floor_cost_threshold -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Create the sender account.""" - return pre.fund_eoa() - - @pytest.fixture def to( request: pytest.FixtureRequest, diff --git a/tests/prague/eip7702_set_code_tx/test_calls.py b/tests/prague/eip7702_set_code_tx/test_calls.py index 6cc4891378d..4bd038293c8 100644 --- a/tests/prague/eip7702_set_code_tx/test_calls.py +++ b/tests/prague/eip7702_set_code_tx/test_calls.py @@ -6,7 +6,6 @@ import pytest from ethereum_test_tools import ( - EOA, Account, Address, Alloc, @@ -53,12 +52,6 @@ def __str__(self) -> str: return f"{self.name}" -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Sender of the transaction.""" - return pre.fund_eoa() - - @pytest.fixture def target_address(pre: Alloc, target_account_type: TargetAccountType) -> Address: """Target address of the call depending on required type of account.""" diff --git a/tests/shanghai/eip3651_warm_coinbase/conftest.py b/tests/shanghai/eip3651_warm_coinbase/conftest.py index ca932155b32..01bdfe3f361 100644 --- a/tests/shanghai/eip3651_warm_coinbase/conftest.py +++ b/tests/shanghai/eip3651_warm_coinbase/conftest.py @@ -2,7 +2,7 @@ import pytest -from ethereum_test_tools import EOA, Alloc, Environment +from ethereum_test_tools import Alloc, Environment @pytest.fixture @@ -15,9 +15,3 @@ def env() -> Environment: def post() -> Alloc: """Post state fixture.""" return Alloc() - - -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Funded EOA used for sending transactions.""" - return pre.fund_eoa() diff --git a/tests/shanghai/eip3855_push0/conftest.py b/tests/shanghai/eip3855_push0/conftest.py index 34b0087d290..a42ce6b5c33 100644 --- a/tests/shanghai/eip3855_push0/conftest.py +++ b/tests/shanghai/eip3855_push0/conftest.py @@ -2,7 +2,7 @@ import pytest -from ethereum_test_tools import EOA, Alloc, Environment +from ethereum_test_tools import Alloc, Environment @pytest.fixture @@ -15,9 +15,3 @@ def env() -> Environment: def post() -> Alloc: """Post state fixture.""" return Alloc() - - -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Funded EOA used for sending transactions.""" - return pre.fund_eoa() diff --git a/tests/shanghai/eip3860_initcode/conftest.py b/tests/shanghai/eip3860_initcode/conftest.py index b6052e6e8ee..c46c898f086 100644 --- a/tests/shanghai/eip3860_initcode/conftest.py +++ b/tests/shanghai/eip3860_initcode/conftest.py @@ -2,7 +2,7 @@ import pytest -from ethereum_test_tools import EOA, Alloc, Environment +from ethereum_test_tools import Alloc, Environment @pytest.fixture @@ -15,9 +15,3 @@ def env() -> Environment: def post() -> Alloc: """Post state fixture.""" return Alloc() - - -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Funded EOA used for sending transactions.""" - return pre.fund_eoa() diff --git a/tests/shanghai/eip4895_withdrawals/conftest.py b/tests/shanghai/eip4895_withdrawals/conftest.py index 8a6eb9a7213..1db1a792374 100644 --- a/tests/shanghai/eip4895_withdrawals/conftest.py +++ b/tests/shanghai/eip4895_withdrawals/conftest.py @@ -2,7 +2,7 @@ import pytest -from ethereum_test_tools import EOA, Alloc, Environment +from ethereum_test_tools import Alloc, Environment @pytest.fixture @@ -15,9 +15,3 @@ def env() -> Environment: def post() -> Alloc: """Post state fixture.""" return Alloc() - - -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Funded EOA used for sending transactions.""" - return pre.fund_eoa() diff --git a/tests/unscheduled/eip7692_eof_v1/eip7069_extcall/test_calls.py b/tests/unscheduled/eip7692_eof_v1/eip7069_extcall/test_calls.py index 2e4e8864ff4..66ebac993a8 100644 --- a/tests/unscheduled/eip7692_eof_v1/eip7069_extcall/test_calls.py +++ b/tests/unscheduled/eip7692_eof_v1/eip7069_extcall/test_calls.py @@ -78,12 +78,6 @@ def __str__(self) -> str: return f"{self.name}" -@pytest.fixture -def sender(pre: Alloc) -> EOA: - """Sender of the transaction.""" - return pre.fund_eoa() - - @pytest.fixture def target_address(pre: Alloc, target_account_type: TargetAccountType) -> Address: """Target address of the call depending on required type of account."""