diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9663d36915..ebae9e14e2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -63,6 +63,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add the `eest make env` command that generates a default env file (`env.yaml`)([#996](https://github.com/ethereum/execution-spec-tests/pull/996)). - ✨ Generate Transaction Test type ([#933](https://github.com/ethereum/execution-spec-tests/pull/933)). - ✨ Add a default location for evm logs (`--evm-dump-dir`) when filling tests ([#999](https://github.com/ethereum/execution-spec-tests/pull/999)). +- ✨ Disable EIP-7742 framework changes for Prague ([#NNN](https://github.com/ethereum/execution-spec-tests/pull/NNN)). ### 🔧 EVM Tools diff --git a/docs/writing_tests/test_markers.md b/docs/writing_tests/test_markers.md index c8af8b3a7e..fb1019657e 100644 --- a/docs/writing_tests/test_markers.md +++ b/docs/writing_tests/test_markers.md @@ -271,6 +271,41 @@ def test_something_with_all_tx_types_but_skip_type_1(state_test_only, tx_type): In this example, the test will be skipped if `tx_type` is equal to 1 by returning a `pytest.mark.skip` marker, and return `None` otherwise. +## Custom Fork Covariant Markers + +Custom fork covariant markers can be created by using the `fork_covariant_parametrize` decorator. + +This decorator takes three arguments: + +- `parameter_names`: A list of parameter names that will be parametrized using the custom function. +- `fn`: A function that takes the fork as parameter and returns a list of values that will be used to parametrize the test. +- `marks`: A marker, list of markers, or a lambda function that can be used to add additional markers to the test. + +```python +import pytest + +from pytest_plugins import fork_covariant_parametrize + +def covariant_function(fork): + return [[1, 2], [3, 4]] if fork.name() == "Paris" else [[4, 5], [5, 6], [6, 7]] + +@fork_covariant_parametrize(parameter_names=[ + "test_parameter", "test_parameter_2" +], fn=covariant_function) +@pytest.mark.valid_from("Paris") +@pytest.mark.valid_until("Shanghai") +def test_case(state_test_only, test_parameter, test_parameter_2): + pass +``` + +In this example, the test will be parametrized with the values `[1, 2]` and `[3, 4]` for the Paris fork, with values `1` and `3` being assigned to `test_parameter` and `2` and `4` being assigned to `test_parameter_2`. For the Shanghai fork, the test will be parametrized with the values `[4, 5]`, `[5, 6]`, and `[6, 7]`. Therefore, more test cases will be generated for the Shanghai fork. + +If the parameters that are being parametrized is only a single parameter, the return value of `fn` should be a list of values for that parameter. + +If the parameters that are being parametrized are multiple, the return value of `fn` should be a list of tuples/lists, where each tuple contains the values for each parameter. + +The function can also return a list of `pytest.param` objects, which allows for additional markers and test IDs to be added to the test. + ## Fill/Execute Markers These markers are used to apply different markers to a test depending on whether it is being filled or executed. diff --git a/src/ethereum_test_fixtures/blockchain.py b/src/ethereum_test_fixtures/blockchain.py index d792748df9..a47a35b716 100644 --- a/src/ethereum_test_fixtures/blockchain.py +++ b/src/ethereum_test_fixtures/blockchain.py @@ -247,7 +247,6 @@ def from_fixture_header( List[Hash], Hash, List[Bytes], - HexNumber, ] # Important: We check EngineNewPayloadV3Parameters first as it has more fields, and pydantic diff --git a/src/ethereum_test_fixtures/tests/test_blockchain.py b/src/ethereum_test_fixtures/tests/test_blockchain.py index 07399604e9..2c610df8f5 100644 --- a/src/ethereum_test_fixtures/tests/test_blockchain.py +++ b/src/ethereum_test_fixtures/tests/test_blockchain.py @@ -16,7 +16,6 @@ Bytes, Hash, HeaderNonce, - HexNumber, TestPrivateKey, ZeroPaddedHexNumber, to_json, @@ -669,7 +668,6 @@ excess_blob_gas=18, parent_beacon_block_root=19, requests_hash=20, - target_blobs_per_block=21, ), transactions=[ Transaction( @@ -731,7 +729,7 @@ "blobGasUsed": hex(17), "excessBlobGas": hex(18), "blockHash": ( - "0x9f6459fb2eca2b75ee861e97d679ba91457bb446c8484a7ad76d1675a7f78fde" + "0x93bd662d8a80a1f54bffc6d140b83d6cda233209998809f9540be51178b4d0b6" ), "transactions": [ Transaction( @@ -789,9 +787,8 @@ ), ).requests_list ], - HexNumber(21).hex(), ], - "forkchoiceUpdatedVersion": "4", + "forkchoiceUpdatedVersion": "3", "newPayloadVersion": "4", "validationError": "BlockException.INCORRECT_BLOCK_FORMAT" "|TransactionException.INTRINSIC_GAS_TOO_LOW", @@ -826,7 +823,6 @@ excess_blob_gas=18, parent_beacon_block_root=19, requests_hash=20, - target_blobs_per_block=21, ), transactions=[ Transaction( @@ -887,7 +883,7 @@ "blobGasUsed": hex(17), "excessBlobGas": hex(18), "blockHash": ( - "0x9f6459fb2eca2b75ee861e97d679ba91457bb446c8484a7ad76d1675a7f78fde" + "0x93bd662d8a80a1f54bffc6d140b83d6cda233209998809f9540be51178b4d0b6" ), "transactions": [ Transaction( @@ -945,10 +941,9 @@ ), ).requests_list ], - HexNumber(21).hex(), ], "newPayloadVersion": "4", - "forkchoiceUpdatedVersion": "4", + "forkchoiceUpdatedVersion": "3", "validationError": "BlockException.INCORRECT_BLOCK_FORMAT" "|TransactionException.INTRINSIC_GAS_TOO_LOW", }, @@ -1232,7 +1227,6 @@ def test_json_deserialization( target_pubkey=BLSPublicKey(2), ), ).requests_list, - HexNumber(9), ), [ { @@ -1298,7 +1292,6 @@ def test_json_deserialization( ), ).requests_list ], - HexNumber(9).hex(), ], id="fixture_engine_new_payload_parameters_v4", ), diff --git a/src/ethereum_test_forks/base_fork.py b/src/ethereum_test_forks/base_fork.py index 776af9b237..8227d40586 100644 --- a/src/ethereum_test_forks/base_fork.py +++ b/src/ethereum_test_forks/base_fork.py @@ -29,7 +29,7 @@ def __call__(self, block_number: int = 0, timestamp: int = 0) -> Any: class MemoryExpansionGasCalculator(Protocol): """ - A protocol to calculate the gas cost of memory expansion for a given fork. + A protocol to calculate the gas cost of memory expansion at a given fork. """ def __call__(self, *, new_bytes: int, previous_bytes: int = 0) -> int: @@ -41,7 +41,7 @@ def __call__(self, *, new_bytes: int, previous_bytes: int = 0) -> int: class CalldataGasCalculator(Protocol): """ - A protocol to calculate the transaction gas cost of calldata for a given fork. + A protocol to calculate the transaction gas cost of calldata at a given fork. """ def __call__(self, *, data: BytesConvertible, floor: bool = False) -> int: @@ -65,7 +65,7 @@ def __call__(self, *, data: BytesConvertible) -> int: class TransactionIntrinsicCostCalculator(Protocol): """ - A protocol to calculate the intrinsic gas cost of a transaction for a given fork. + A protocol to calculate the intrinsic gas cost of a transaction at a given fork. """ def __call__( @@ -97,6 +97,37 @@ def __call__( pass +class BlobGasPriceCalculator(Protocol): + """ + A protocol to calculate the blob gas price given the excess blob gas at a given fork. + """ + + def __call__(self, *, excess_blob_gas: int) -> int: + """ + Returns the blob gas price given the excess blob gas. + """ + pass + + +class ExcessBlobGasCalculator(Protocol): + """ + A protocol to calculate the excess blob gas for a block at a given fork. + """ + + def __call__( + self, + *, + parent_excess_blob_gas: int | None = None, + parent_excess_blobs: int | None = None, + parent_blob_gas_used: int | None = None, + parent_blob_count: int | None = None, + ) -> int: + """ + Returns the excess blob gas given the parent's excess blob gas and blob gas used. + """ + pass + + class BaseForkMeta(ABCMeta): """ Metaclass for BaseFork @@ -218,7 +249,7 @@ def header_blob_gas_used_required(cls, block_number: int = 0, timestamp: int = 0 @classmethod @abstractmethod - def header_beacon_root_required(cls, block_number: int, timestamp: int) -> bool: + def header_beacon_root_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: """ Returns true if the header must contain parent beacon block root """ @@ -226,12 +257,22 @@ def header_beacon_root_required(cls, block_number: int, timestamp: int) -> bool: @classmethod @abstractmethod - def header_requests_required(cls, block_number: int, timestamp: int) -> bool: + def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: """ Returns true if the header must contain beacon chain requests """ pass + @classmethod + @abstractmethod + def header_target_blobs_per_block_required( + cls, block_number: int = 0, timestamp: int = 0 + ) -> bool: + """ + Returns true if the header must contain target blobs per block. + """ + pass + # Gas related abstract methods @classmethod @@ -285,33 +326,61 @@ def transaction_intrinsic_cost_calculator( @classmethod @abstractmethod - def header_target_blobs_per_block_required(cls, block_number: int, timestamp: int) -> bool: + def blob_gas_price_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> BlobGasPriceCalculator: """ - Returns true if the header must contain target blobs per block. + Returns a callable that calculates the blob gas price at a given fork. + """ + pass + + @classmethod + @abstractmethod + def excess_blob_gas_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> ExcessBlobGasCalculator: + """ + Returns a callable that calculates the excess blob gas for a block at a given fork. + """ + pass + + @classmethod + @abstractmethod + def min_base_fee_per_blob_gas(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the minimum base fee per blob gas at a given fork. + """ + pass + + @classmethod + @abstractmethod + def blob_gas_per_blob(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the amount of blob gas used per blob at a given fork. """ pass @classmethod @abstractmethod - def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: + def blob_base_fee_update_fraction(cls, block_number: int = 0, timestamp: int = 0) -> int: """ - Returns the amount of blob gas used per blob for a given fork. + Returns the blob base fee update fraction at a given fork. """ pass @classmethod @abstractmethod - def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int: + def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: """ - Returns the target blobs per block for a given fork. + Returns the target blobs per block at a given fork. """ pass @classmethod @abstractmethod - def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int: + def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: """ - Returns the max blobs per block for a given fork. + Returns the max blobs per block at a given fork. """ pass diff --git a/src/ethereum_test_forks/forks/forks.py b/src/ethereum_test_forks/forks/forks.py index 523983ce2c..2563c311a4 100644 --- a/src/ethereum_test_forks/forks/forks.py +++ b/src/ethereum_test_forks/forks/forks.py @@ -16,25 +16,20 @@ from ..base_fork import ( BaseFork, + BlobGasPriceCalculator, CalldataGasCalculator, + ExcessBlobGasCalculator, MemoryExpansionGasCalculator, TransactionDataFloorCostCalculator, TransactionIntrinsicCostCalculator, ) from ..gas_costs import GasCosts +from .helpers import ceiling_division, fake_exponential CURRENT_FILE = Path(realpath(__file__)) CURRENT_FOLDER = CURRENT_FILE.parent -def ceiling_division(a: int, b: int) -> int: - """ - Calculates the ceil without using floating point. - Used by many of the EVM's formulas - """ - return -(a // -b) - - # All forks must be listed here !!! in the order they were introduced !!! class Frontier(BaseFork, solc_name="homestead"): """ @@ -245,28 +240,60 @@ def fn( return fn @classmethod - def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: + def blob_gas_price_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> BlobGasPriceCalculator: """ - Returns the amount of blob gas used per blob for a given fork. + Returns a callable that calculates the blob gas price at a given fork. """ - return 0 + raise NotImplementedError("Blob gas price calculator is not supported in Frontier") @classmethod - def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int: + def excess_blob_gas_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> ExcessBlobGasCalculator: """ - Returns the target number of blobs per block for a given fork. + Returns a callable that calculates the excess blob gas for a block at a given fork. """ - return 0 + raise NotImplementedError("Excess blob gas calculator is not supported in Frontier") + + @classmethod + def min_base_fee_per_blob_gas(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the amount of blob gas used per blob at a given fork. + """ + raise NotImplementedError("Base fee per blob gas is not supported in Frontier") @classmethod - def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int: + def blob_base_fee_update_fraction(cls, block_number: int = 0, timestamp: int = 0) -> int: """ - Returns the max number of blobs per block for a given fork. + Returns the blob base fee update fraction at a given fork. + """ + raise NotImplementedError("Blob base fee update fraction is not supported in Frontier") + + @classmethod + def blob_gas_per_blob(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the amount of blob gas used per blob at a given fork. """ return 0 @classmethod - def header_requests_required(cls, block_number: int, timestamp: int) -> bool: + def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the target number of blobs per block at a given fork. + """ + raise NotImplementedError("Target blobs per block is not supported in Frontier") + + @classmethod + def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the max number of blobs per block at a given fork. + """ + raise NotImplementedError("Max blobs per block is not supported in Frontier") + + @classmethod + def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: """ At genesis, header must not contain beacon chain requests. """ @@ -1011,21 +1038,85 @@ def header_beacon_root_required(cls, block_number: int = 0, timestamp: int = 0) return True @classmethod - def blob_gas_per_blob(cls, block_number: int, timestamp: int) -> int: + def blob_gas_price_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> BlobGasPriceCalculator: + """ + Returns a callable that calculates the blob gas price at Cancun. + """ + min_base_fee_per_blob_gas = cls.min_base_fee_per_blob_gas(block_number, timestamp) + blob_base_fee_update_fraction = cls.blob_base_fee_update_fraction(block_number, timestamp) + + def fn(*, excess_blob_gas) -> int: + return fake_exponential( + min_base_fee_per_blob_gas, + excess_blob_gas, + blob_base_fee_update_fraction, + ) + + return fn + + @classmethod + def excess_blob_gas_calculator( + cls, block_number: int = 0, timestamp: int = 0 + ) -> ExcessBlobGasCalculator: + """ + Returns a callable that calculates the excess blob gas for a block at Cancun. + """ + target_blobs_per_block = cls.target_blobs_per_block(block_number, timestamp) + blob_gas_per_blob = cls.blob_gas_per_blob(block_number, timestamp) + target_blob_gas_per_block = target_blobs_per_block * blob_gas_per_blob + + def fn( + *, + parent_excess_blob_gas: int | None = None, + parent_excess_blobs: int | None = None, + parent_blob_gas_used: int | None = None, + parent_blob_count: int | None = None, + ) -> int: + if parent_excess_blob_gas is None: + assert parent_excess_blobs is not None, "Parent excess blobs are required" + parent_excess_blob_gas = parent_excess_blobs * blob_gas_per_blob + if parent_blob_gas_used is None: + assert parent_blob_count is not None, "Parent blob count is required" + parent_blob_gas_used = parent_blob_count * blob_gas_per_blob + if parent_excess_blob_gas + parent_blob_gas_used < target_blob_gas_per_block: + return 0 + else: + return parent_excess_blob_gas + parent_blob_gas_used - target_blob_gas_per_block + + return fn + + @classmethod + def min_base_fee_per_blob_gas(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the minimum base fee per blob gas for Cancun. + """ + return 1 + + @classmethod + def blob_base_fee_update_fraction(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the blob base fee update fraction for Cancun. + """ + return 3338477 + + @classmethod + def blob_gas_per_blob(cls, block_number: int = 0, timestamp: int = 0) -> int: """ Blobs are enabled starting from Cancun. """ return 2**17 @classmethod - def target_blobs_per_block(cls, block_number: int, timestamp: int) -> int: + def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: """ Blobs are enabled starting from Cancun, with a static target of 3 blobs. """ return 3 @classmethod - def max_blobs_per_block(cls, block_number: int, timestamp: int) -> int: + def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: """ Blobs are enabled starting from Cancun, with a static max of 6 blobs. """ @@ -1256,6 +1347,27 @@ def fn( return fn + @classmethod + def blob_base_fee_update_fraction(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Returns the blob base fee update fraction for Prague. + """ + return 5007716 + + @classmethod + def target_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Target blob count of 6 for Prague. + """ + return 6 + + @classmethod + def max_blobs_per_block(cls, block_number: int = 0, timestamp: int = 0) -> int: + """ + Max blob count of 9 for Prague. + """ + return 9 + @classmethod def pre_allocation_blockchain(cls) -> Mapping: """ @@ -1319,25 +1431,13 @@ def pre_allocation_blockchain(cls) -> Mapping: return new_allocation | super(Prague, cls).pre_allocation_blockchain() # type: ignore @classmethod - def header_requests_required(cls, block_number: int, timestamp: int) -> bool: + def header_requests_required(cls, block_number: int = 0, timestamp: int = 0) -> bool: """ Prague requires that the execution layer header contains the beacon chain requests hash. """ return True - @classmethod - def header_target_blobs_per_block_required( - cls, - block_number: int = 0, - timestamp: int = 0, - ) -> bool: - """ - Prague requires that the execution layer header contains the beacon - chain target blobs per block. - """ - return True - @classmethod def engine_new_payload_requests(cls, block_number: int = 0, timestamp: int = 0) -> bool: """ @@ -1345,15 +1445,6 @@ def engine_new_payload_requests(cls, block_number: int = 0, timestamp: int = 0) """ return True - @classmethod - def engine_new_payload_target_blobs_per_block( - cls, block_number: int = 0, timestamp: int = 0 - ) -> bool: - """ - Starting at Prague, new payloads include the target blobs per block as a parameter. - """ - return True - @classmethod def engine_new_payload_version( cls, block_number: int = 0, timestamp: int = 0 @@ -1364,22 +1455,13 @@ def engine_new_payload_version( return 4 @classmethod - def engine_payload_attribute_target_blobs_per_block( - cls, block_number: int = 0, timestamp: int = 0 - ) -> bool: - """ - Starting at Prague, payload attributes include the target blobs per block. - """ - return True - - @classmethod - def engine_payload_attribute_max_blobs_per_block( + def engine_forkchoice_updated_version( cls, block_number: int = 0, timestamp: int = 0 - ) -> bool: + ) -> Optional[int]: """ - Starting at Prague, payload attributes include the max blobs per block. + At Prague, version number of NewPayload and ForkchoiceUpdated diverge. """ - return True + return 3 class CancunEIP7692( # noqa: SC200 diff --git a/src/ethereum_test_forks/forks/helpers.py b/src/ethereum_test_forks/forks/helpers.py new file mode 100644 index 0000000000..9e970e1a4a --- /dev/null +++ b/src/ethereum_test_forks/forks/helpers.py @@ -0,0 +1,25 @@ +""" +Helpers used to return fork-specific values. +""" + + +def ceiling_division(a: int, b: int) -> int: + """ + Calculates the ceil without using floating point. + Used by many of the EVM's formulas + """ + return -(a // -b) + + +def fake_exponential(factor: int, numerator: int, denominator: int) -> int: + """ + Used to calculate the blob gas cost. + """ + i = 1 + output = 0 + numerator_accumulator = factor * denominator + while numerator_accumulator > 0: + output += numerator_accumulator + numerator_accumulator = (numerator_accumulator * numerator) // (denominator * i) + i += 1 + return output // denominator diff --git a/src/ethereum_test_specs/state.py b/src/ethereum_test_specs/state.py index 4595d2f6bb..2b741b3923 100644 --- a/src/ethereum_test_specs/state.py +++ b/src/ethereum_test_specs/state.py @@ -32,8 +32,6 @@ from .debugging import print_traces from .helpers import verify_transactions -TARGET_BLOB_GAS_PER_BLOCK = 393216 - class StateTest(BaseTest): """ @@ -58,7 +56,7 @@ class StateTest(BaseTest): TransactionPost, ] - def _generate_blockchain_genesis_environment(self) -> Environment: + def _generate_blockchain_genesis_environment(self, *, fork: Fork) -> Environment: """ Generate the genesis environment for the BlockchainTest formatted test. """ @@ -79,8 +77,8 @@ def _generate_blockchain_genesis_environment(self) -> Environment: # set the excess blob gas by setting the excess blob gas of the genesis block # to the expected value plus the TARGET_BLOB_GAS_PER_BLOCK, which is the value # that will be subtracted from the excess blob gas when the first block is mined. - updated_values["excess_blob_gas"] = ( - self.env.excess_blob_gas + TARGET_BLOB_GAS_PER_BLOCK + updated_values["excess_blob_gas"] = self.env.excess_blob_gas + ( + fork.target_blobs_per_block() * fork.blob_gas_per_blob() ) return self.env.copy(**updated_values) @@ -107,12 +105,12 @@ def _generate_blockchain_blocks(self) -> List[Block]: ) ] - def generate_blockchain_test(self) -> BlockchainTest: + def generate_blockchain_test(self, *, fork: Fork) -> BlockchainTest: """ Generate a BlockchainTest fixture from this StateTest fixture. """ return BlockchainTest( - genesis_environment=self._generate_blockchain_genesis_environment(), + genesis_environment=self._generate_blockchain_genesis_environment(fork=fork), pre=self.pre, post=self.post, blocks=self._generate_blockchain_blocks(), @@ -195,7 +193,7 @@ def generate( Generate the BlockchainTest fixture. """ if fixture_format in BlockchainTest.supported_fixture_formats: - return self.generate_blockchain_test().generate( + return self.generate_blockchain_test(fork=fork).generate( request=request, t8n=t8n, fork=fork, fixture_format=fixture_format, eips=eips ) elif fixture_format == StateFixture: diff --git a/src/pytest_plugins/__init__.py b/src/pytest_plugins/__init__.py index f77be41e13..6b02eb34c7 100644 --- a/src/pytest_plugins/__init__.py +++ b/src/pytest_plugins/__init__.py @@ -1,3 +1,7 @@ """ Package containing pytest plugins related to test filling. """ + +from .forks import fork_covariant_parametrize + +__all__ = ["fork_covariant_parametrize"] diff --git a/src/pytest_plugins/forks/__init__.py b/src/pytest_plugins/forks/__init__.py index 7ce63ea627..eb404537ff 100644 --- a/src/pytest_plugins/forks/__init__.py +++ b/src/pytest_plugins/forks/__init__.py @@ -3,3 +3,7 @@ tests based on the user-provided fork range the tests' specified validity markers. """ + +from .forks import fork_covariant_parametrize + +__all__ = ["fork_covariant_parametrize"] diff --git a/src/pytest_plugins/forks/forks.py b/src/pytest_plugins/forks/forks.py index 4a1afa9f61..4307200be5 100644 --- a/src/pytest_plugins/forks/forks.py +++ b/src/pytest_plugins/forks/forks.py @@ -7,7 +7,7 @@ import textwrap from dataclasses import dataclass, field from types import FunctionType -from typing import Any, Callable, List, Set, Tuple +from typing import Any, Callable, ClassVar, Iterable, List, Set, Tuple, Type import pytest from _pytest.mark.structures import ParameterSet @@ -16,7 +16,6 @@ from ethereum_clis import TransitionTool from ethereum_test_forks import ( Fork, - ForkAttribute, get_deployed_forks, get_forks, get_forks_with_no_parents, @@ -62,18 +61,6 @@ def pytest_addoption(parser): ) -@dataclass(kw_only=True) -class MarkedValue: - """ - A processed value for a covariant parameter. - - Value can be a list for inclusive parameters. - """ - - value: Any - marks: List[pytest.Mark | pytest.MarkDecorator] = field(default_factory=list) - - @dataclass(kw_only=True) class ForkCovariantParameter: """ @@ -81,10 +68,9 @@ class ForkCovariantParameter: """ names: List[str] - values: List[List[MarkedValue]] + values: List[ParameterSet] -@dataclass(kw_only=True) class ForkParametrizer: """ A parametrizer for a test case that is parametrized by the fork. @@ -92,14 +78,32 @@ class ForkParametrizer: fork: Fork fork_covariant_parameters: List[ForkCovariantParameter] = field(default_factory=list) - marks: List[pytest.MarkDecorator | pytest.Mark] = field(default_factory=list) + + def __init__( + self, + fork: Fork, + marks: List[pytest.MarkDecorator | pytest.Mark] = [], + fork_covariant_parameters: List[ForkCovariantParameter] = [], + ): + self.fork_covariant_parameters = [ + ForkCovariantParameter( + names=["fork"], + values=[ + pytest.param( + fork, + marks=marks, + ) + ], + ) + ] + fork_covariant_parameters + self.fork = fork @property def parameter_names(self) -> List[str]: """ Return the parameter names for the test case. """ - parameter_names = ["fork"] + parameter_names = [] for p in self.fork_covariant_parameters: parameter_names.extend(p.names) return parameter_names @@ -109,105 +113,146 @@ def parameter_values(self) -> List[ParameterSet]: """ Return the parameter values for the test case. """ - param_value_combinations = [ - # Flatten the list of values for each parameter - list(itertools.chain(*params)) - for params in itertools.product( - # Add the fork so it is multiplied by the other parameters. - # It's a list of lists because all parameters are, but it will - # flattened after the product. - [[MarkedValue(value=self.fork)]], - # Add the values for each parameter, all of them are lists of at least one element. - *[p.values for p in self.fork_covariant_parameters], - ) - ] + parameter_set_combinations = itertools.product( + # Add the values for each parameter, all of them are lists of at least one element. + *[p.values for p in self.fork_covariant_parameters], + ) parameter_set_list: List[ParameterSet] = [] - for marked_params in param_value_combinations: - marks = self.marks.copy() + for parameter_set_combination in parameter_set_combinations: params: List[Any] = [] - for p in marked_params: - params.append(p.value) + marks: List[pytest.Mark | pytest.MarkDecorator] = [] + id: str | None = None + for p in parameter_set_combination: + assert isinstance(p, ParameterSet) + params.extend(p.values) if p.marks: marks.extend(p.marks) - parameter_set_list.append(pytest.param(*params, marks=marks)) + if p.id: + if id is None: + id = f"fork_{self.fork.name()}-{p.id}" + else: + id = f"{id}-{p.id}" + parameter_set_list.append(pytest.param(*params, marks=marks, id=id)) return parameter_set_list -@dataclass(kw_only=True) class CovariantDescriptor: """ A descriptor for a parameter that is covariant with the fork: the parametrized values change depending on the fork. """ - marker_name: str - description: str - fork_attribute_name: str - parameter_names: List[str] - - def get_marker(self, metafunc: Metafunc) -> pytest.Mark | None: - """ - Get the marker for the given test function. - """ - m = metafunc.definition.iter_markers(self.marker_name) - if m is None: - return None - marker_list = list(m) - assert len(marker_list) <= 1, f"Multiple markers {self.marker_name} found" - if len(marker_list) == 0: - return None - return marker_list[0] + parameter_names: List[str] = [] + fn: Callable[[Fork], List[Any] | Iterable[Any]] | None = None - def check_enabled(self, metafunc: Metafunc) -> bool: - """ - Check if the marker is enabled for the given test function. - """ - return self.get_marker(metafunc) is not None + selector: FunctionType | None = None + marks: None | pytest.Mark | pytest.MarkDecorator | List[ + pytest.Mark | pytest.MarkDecorator + ] = None - @staticmethod - def process_value( - values: Any | List[Any] | Tuple[Any], - selector: FunctionType, + def __init__( + self, + parameter_names: List[str] | str, + fn: Callable[[Fork], List[Any] | Iterable[Any]] | None = None, + selector: FunctionType | None = None, marks: None | pytest.Mark | pytest.MarkDecorator - | List[pytest.Mark | pytest.MarkDecorator] - | Callable[ - [Any], - List[pytest.Mark | pytest.MarkDecorator] | pytest.Mark | pytest.MarkDecorator | None, - ], - ) -> List[List[MarkedValue]]: + | List[pytest.Mark | pytest.MarkDecorator] = None, + ): + if isinstance(parameter_names, str): + self.parameter_names = parameter_names.split(",") + else: + self.parameter_names = parameter_names + self.fn = fn + self.selector = selector + self.marks = marks + + def process_value( + self, + parameters_values: Any | List[Any] | Tuple[Any] | ParameterSet, + ) -> ParameterSet | None: """ Process a value for a covariant parameter. - The `selector` is applied to values in order to filter them. + The `selector` is applied to parameters_values in order to filter them. """ - if not isinstance(values, tuple) and not isinstance(values, list): - values = [values] - - if selector(*values[: selector.__code__.co_argcount]): + if isinstance(parameters_values, ParameterSet): + return parameters_values + + if len(self.parameter_names) == 1: + # Wrap values that are meant for a single parameter in a list + parameters_values = [parameters_values] + marks = self.marks + if self.selector is None or self.selector( + *parameters_values[: self.selector.__code__.co_argcount] # type: ignore + ): if isinstance(marks, FunctionType): - marks = marks(*values[: marks.__code__.co_argcount]) + marks = marks(*parameters_values[: marks.__code__.co_argcount]) assert not isinstance(marks, FunctionType), "marks must be a list or None" if marks is None: marks = [] elif not isinstance(marks, list): marks = [marks] # type: ignore - return [[MarkedValue(value=v, marks=marks) for v in values]] + return pytest.param(*parameters_values, marks=marks) - return [] + return None - def process_values(self, metafunc: Metafunc, values: List[Any]) -> List[List[MarkedValue]]: + def process_values(self, values: Iterable[Any]) -> List[ParameterSet]: """ Filter the values for the covariant parameter. I.e. if the marker has an argument, the argument is interpreted as a lambda function that filters the values. """ - marker = self.get_marker(metafunc) + processed_values: List[ParameterSet] = [] + for value in values: + processed_value = self.process_value(value) + if processed_value is not None: + processed_values.append(processed_value) + return processed_values + + def add_values(self, fork_parametrizer: ForkParametrizer) -> None: + """ + Add the values for the covariant parameter to the parametrizer. + """ + if self.fn is None: + return + fork = fork_parametrizer.fork + values = self.fn(fork) + values = self.process_values(values) + assert len(values) > 0 + fork_parametrizer.fork_covariant_parameters.append( + ForkCovariantParameter(names=self.parameter_names, values=values) + ) + + +class CovariantDecorator(CovariantDescriptor): + """ + A marker used to parametrize a function by a covariant parameter with the values + returned by a fork method. + """ + + marker_name: ClassVar[str] + description: ClassVar[str] + fork_attribute_name: ClassVar[str] + marker_parameter_names: ClassVar[List[str]] + + def __init__(self, metafunc: Metafunc): + self.metafunc = metafunc + + m = metafunc.definition.iter_markers(self.marker_name) + if m is None: + return + marker_list = list(m) + assert len(marker_list) <= 1, f"Multiple markers {self.marker_name} found" + if len(marker_list) == 0: + return + marker = marker_list[0] + assert marker is not None assert len(marker.args) == 0, "Only keyword arguments are supported" @@ -221,73 +266,82 @@ def process_values(self, metafunc: Metafunc, values: List[Any]) -> List[List[Mar if len(kwargs) > 0: raise ValueError(f"Unknown arguments to {self.marker_name}: {kwargs}") - processed_values: List[List[MarkedValue]] = [] - for value in values: - processed_values.extend(self.process_value(value, selector, marks)) - - return processed_values + def fn(fork: Fork) -> List[Any]: + return getattr(fork, self.fork_attribute_name)(block_number=0, timestamp=0) - def add_values(self, metafunc: Metafunc, fork_parametrizer: ForkParametrizer) -> None: - """ - Add the values for the covariant parameter to the parametrizer. - """ - if not self.check_enabled(metafunc=metafunc): - return - fork = fork_parametrizer.fork - get_fork_covariant_values: ForkAttribute = getattr(fork, self.fork_attribute_name) - values = get_fork_covariant_values(block_number=0, timestamp=0) - assert isinstance(values, list) - assert len(values) > 0 - values = self.process_values(metafunc, values) - fork_parametrizer.fork_covariant_parameters.append( - ForkCovariantParameter(names=self.parameter_names, values=values) + super().__init__( + parameter_names=self.marker_parameter_names, + fn=fn, + selector=selector, + marks=marks, ) -fork_covariant_descriptors = [ - CovariantDescriptor( +def covariant_decorator( + marker_name: str, + description: str, + fork_attribute_name: str, + parameter_names: List[str], +) -> Type[CovariantDecorator]: + """ + Create a decorator class for a covariant parameter. + """ + return type( + marker_name, + (CovariantDecorator,), + { + "marker_name": marker_name, + "description": description, + "fork_attribute_name": fork_attribute_name, + "marker_parameter_names": parameter_names, + }, + ) + + +fork_covariant_decorators: List[Type[CovariantDecorator]] = [ + covariant_decorator( marker_name="with_all_tx_types", description="marks a test to be parametrized for all tx types at parameter named tx_type" " of type int", fork_attribute_name="tx_types", parameter_names=["tx_type"], ), - CovariantDescriptor( + covariant_decorator( marker_name="with_all_contract_creating_tx_types", description="marks a test to be parametrized for all tx types that can create a contract" " at parameter named tx_type of type int", fork_attribute_name="contract_creating_tx_types", parameter_names=["tx_type"], ), - CovariantDescriptor( + covariant_decorator( marker_name="with_all_precompiles", description="marks a test to be parametrized for all precompiles at parameter named" " precompile of type int", fork_attribute_name="precompiles", parameter_names=["precompile"], ), - CovariantDescriptor( + covariant_decorator( marker_name="with_all_evm_code_types", description="marks a test to be parametrized for all EVM code types at parameter named" " `evm_code_type` of type `EVMCodeType`, such as `LEGACY` and `EOF_V1`", fork_attribute_name="evm_code_types", parameter_names=["evm_code_type"], ), - CovariantDescriptor( + covariant_decorator( marker_name="with_all_call_opcodes", description="marks a test to be parametrized for all *CALL opcodes at parameter named" " call_opcode, and also the appropriate EVM code type at parameter named evm_code_type", fork_attribute_name="call_opcodes", parameter_names=["call_opcode", "evm_code_type"], ), - CovariantDescriptor( + covariant_decorator( marker_name="with_all_create_opcodes", description="marks a test to be parametrized for all *CREATE* opcodes at parameter named" " create_opcode, and also the appropriate EVM code type at parameter named evm_code_type", fork_attribute_name="create_opcodes", parameter_names=["create_opcode", "evm_code_type"], ), - CovariantDescriptor( + covariant_decorator( marker_name="with_all_system_contracts", description="marks a test to be parametrized for all system contracts at parameter named" " system_contract of type int", @@ -297,6 +351,50 @@ def add_values(self, metafunc: Metafunc, fork_parametrizer: ForkParametrizer) -> ] +FORK_COVARIANT_PARAMETRIZE_ATTRIBUTE = "fork_covariant_parametrize" + + +def fork_covariant_parametrize( + *, + parameter_names: List[str] | str, + fn: Callable[[Fork], List[Any] | Iterable[Any]], + marks: None + | pytest.Mark + | pytest.MarkDecorator + | List[pytest.Mark | pytest.MarkDecorator] = None, +): + """ + Decorator to parametrize a test function by covariant parameters. + + The decorated function will be parametrized by the values returned by the `fn` function + for each fork. + + If the parameters that are being parametrized is only a single parameter, the return value + of `fn` should be a list of values for that parameter. + + If the parameters that are being parametrized are multiple, the return value of `fn` should + be a list of tuples/lists, where each tuple contains the values for each parameter. + """ + + def decorator(decorated_function: FunctionType) -> FunctionType: + """ + Decorator to parametrize a test function by covariant parameters. + """ + covariant_descriptor = CovariantDescriptor( + parameter_names=parameter_names, + fn=fn, + marks=marks, + ) + covariant_descriptors: List[CovariantDescriptor] = getattr( + decorated_function, FORK_COVARIANT_PARAMETRIZE_ATTRIBUTE, [] + ) + covariant_descriptors.append(covariant_descriptor) + setattr(decorated_function, FORK_COVARIANT_PARAMETRIZE_ATTRIBUTE, covariant_descriptors) + return decorated_function + + return decorator + + @pytest.hookimpl(tryfirst=True) def pytest_configure(config: pytest.Config): """ @@ -321,7 +419,7 @@ def pytest_configure(config: pytest.Config): "valid_until(fork): specifies until which fork a test case is valid", ) - for d in fork_covariant_descriptors: + for d in fork_covariant_decorators: config.addinivalue_line("markers", f"{d.marker_name}: {d.description}") forks = set([fork for fork in get_forks() if not fork.ignore()]) @@ -618,9 +716,17 @@ def add_fork_covariant_parameters( """ Iterate over the fork covariant descriptors and add their values to the test function. """ - for covariant_descriptor in fork_covariant_descriptors: + for covariant_descriptor in fork_covariant_decorators: for fork_parametrizer in fork_parametrizers: - covariant_descriptor.add_values(metafunc=metafunc, fork_parametrizer=fork_parametrizer) + covariant_descriptor(metafunc=metafunc).add_values(fork_parametrizer=fork_parametrizer) + + if hasattr(metafunc.function, FORK_COVARIANT_PARAMETRIZE_ATTRIBUTE): + covariant_descriptors: List[CovariantDescriptor] = getattr( + metafunc.function, FORK_COVARIANT_PARAMETRIZE_ATTRIBUTE + ) + for descriptor in covariant_descriptors: + for fork_parametrizer in fork_parametrizers: + descriptor.add_values(fork_parametrizer=fork_parametrizer) def parameters_from_fork_parametrizer_list( diff --git a/src/pytest_plugins/forks/tests/test_covariant_markers.py b/src/pytest_plugins/forks/tests/test_covariant_markers.py index 1ede8a7be6..6a32157896 100644 --- a/src/pytest_plugins/forks/tests/test_covariant_markers.py +++ b/src/pytest_plugins/forks/tests/test_covariant_markers.py @@ -286,6 +286,100 @@ def test_case(state_test_only, tx_type): "Only keyword arguments are supported", id="selector_as_positional_argument", ), + pytest.param( + """ + import pytest + + from pytest_plugins import fork_covariant_parametrize + + def covariant_function(fork): + return [1, 2] if fork.name() == "Paris" else [3, 4, 5] + + @fork_covariant_parametrize(parameter_names=["test_parameter"], fn=covariant_function) + @pytest.mark.valid_from("Paris") + @pytest.mark.valid_until("Shanghai") + def test_case(state_test_only, test_parameter): + pass + """, + dict(passed=5, failed=0, skipped=0, errors=0), + None, + id="custom_covariant_marker", + ), + pytest.param( + """ + import pytest + + from pytest_plugins import fork_covariant_parametrize + + def covariant_function(fork): + return [[1, 2], [3, 4]] if fork.name() == "Paris" else [[4, 5], [5, 6], [6, 7]] + + @fork_covariant_parametrize(parameter_names=[ + "test_parameter", "test_parameter_2" + ], fn=covariant_function) + @pytest.mark.valid_from("Paris") + @pytest.mark.valid_until("Shanghai") + def test_case(state_test_only, test_parameter, test_parameter_2): + pass + """, + dict(passed=5, failed=0, skipped=0, errors=0), + None, + id="multi_parameter_custom_covariant_marker", + ), + pytest.param( + """ + import pytest + + from pytest_plugins import fork_covariant_parametrize + + def covariant_function(fork): + return [ + pytest.param(1, id="first_value"), + 2, + ] if fork.name() == "Paris" else [ + pytest.param(3, id="third_value"), + 4, + 5, + ] + + @fork_covariant_parametrize(parameter_names=["test_parameter"], fn=covariant_function) + @pytest.mark.valid_from("Paris") + @pytest.mark.valid_until("Shanghai") + def test_case(state_test_only, test_parameter): + pass + """, + dict(passed=5, failed=0, skipped=0, errors=0), + None, + id="custom_covariant_marker_pytest_param_id", + ), + pytest.param( + """ + import pytest + + from pytest_plugins import fork_covariant_parametrize + + def covariant_function(fork): + return [ + pytest.param(1, 2, id="first_test"), + pytest.param(3, 4, id="second_test"), + ] if fork.name() == "Paris" else [ + pytest.param(4, 5, id="fourth_test"), + pytest.param(5, 6, id="fifth_test"), + pytest.param(6, 7, id="sixth_test"), + ] + + @fork_covariant_parametrize(parameter_names=[ + "test_parameter", "test_parameter_2" + ], fn=covariant_function) + @pytest.mark.valid_from("Paris") + @pytest.mark.valid_until("Shanghai") + def test_case(state_test_only, test_parameter, test_parameter_2): + pass + """, + dict(passed=5, failed=0, skipped=0, errors=0), + None, + id="multi_parameter_custom_covariant_marker_pytest_param_id", + ), ], ) def test_fork_covariant_markers( diff --git a/src/pytest_plugins/forks/tests/test_fork_parametrizer_types.py b/src/pytest_plugins/forks/tests/test_fork_parametrizer_types.py index 3ab2a9c84c..0aaf8ed85a 100644 --- a/src/pytest_plugins/forks/tests/test_fork_parametrizer_types.py +++ b/src/pytest_plugins/forks/tests/test_fork_parametrizer_types.py @@ -12,7 +12,6 @@ from ..forks import ( ForkCovariantParameter, ForkParametrizer, - MarkedValue, parameters_from_fork_parametrizer_list, ) @@ -31,9 +30,7 @@ ForkParametrizer( fork=Frontier, fork_covariant_parameters=[ - ForkCovariantParameter( - names=["some_value"], values=[[MarkedValue(value=1)]] - ) + ForkCovariantParameter(names=["some_value"], values=[pytest.param(1)]) ], ) ], @@ -48,7 +45,7 @@ fork_covariant_parameters=[ ForkCovariantParameter( names=["some_value"], - values=[[MarkedValue(value=1)], [MarkedValue(value=2)]], + values=[pytest.param(1), pytest.param(2)], ) ], ) @@ -65,8 +62,8 @@ ForkCovariantParameter( names=["some_value"], values=[ - [MarkedValue(value=1, marks=[pytest.mark.some_mark])], - [MarkedValue(value=2)], + pytest.param(1, marks=[pytest.mark.some_mark]), + pytest.param(2), ], ) ], @@ -81,12 +78,8 @@ ForkParametrizer( fork=Frontier, fork_covariant_parameters=[ - ForkCovariantParameter( - names=["some_value"], values=[[MarkedValue(value=1)]] - ), - ForkCovariantParameter( - names=["another_value"], values=[[MarkedValue(value=2)]] - ), + ForkCovariantParameter(names=["some_value"], values=[pytest.param(1)]), + ForkCovariantParameter(names=["another_value"], values=[pytest.param(2)]), ], ) ], @@ -99,12 +92,10 @@ ForkParametrizer( fork=Frontier, fork_covariant_parameters=[ - ForkCovariantParameter( - names=["some_value"], values=[[MarkedValue(value=1)]] - ), + ForkCovariantParameter(names=["some_value"], values=[pytest.param(1)]), ForkCovariantParameter( names=["another_value"], - values=[[MarkedValue(value=2)], [MarkedValue(value=3)]], + values=[pytest.param(2), pytest.param(3)], ), ], ) @@ -121,8 +112,8 @@ ForkCovariantParameter( names=["some_value", "another_value"], values=[ - [MarkedValue(value=1), MarkedValue(value="a")], - [MarkedValue(value=2), MarkedValue(value="b")], + pytest.param(1, "a"), + pytest.param(2, "b"), ], ) ], @@ -140,15 +131,15 @@ ForkCovariantParameter( names=["some_value", "another_value"], values=[ - [MarkedValue(value=1), MarkedValue(value="a")], - [MarkedValue(value=2), MarkedValue(value="b")], + pytest.param(1, "a"), + pytest.param(2, "b"), ], ), ForkCovariantParameter( names=["yet_another_value", "last_value"], values=[ - [MarkedValue(value=3), MarkedValue(value="x")], - [MarkedValue(value=4), MarkedValue(value="y")], + pytest.param(3, "x"), + pytest.param(4, "y"), ], ), ], @@ -171,15 +162,15 @@ ForkCovariantParameter( names=["shared_value", "different_value_1"], values=[ - [MarkedValue(value=1), MarkedValue(value="a")], - [MarkedValue(value=2), MarkedValue(value="b")], + pytest.param(1, "a"), + pytest.param(2, "b"), ], ), ForkCovariantParameter( names=["shared_value", "different_value_2"], values=[ - [MarkedValue(value=1), MarkedValue(value="x")], - [MarkedValue(value=2), MarkedValue(value="y")], + pytest.param(1, "x"), + pytest.param(2, "y"), ], ), ], diff --git a/tests/cancun/eip4788_beacon_root/conftest.py b/tests/cancun/eip4788_beacon_root/conftest.py index c286bff0d1..4f06c608e5 100644 --- a/tests/cancun/eip4788_beacon_root/conftest.py +++ b/tests/cancun/eip4788_beacon_root/conftest.py @@ -7,20 +7,10 @@ import pytest -from ethereum_test_tools import ( - AccessList, - Account, - Address, - Alloc, - Bytecode, - Environment, - Hash, - Storage, - Transaction, - add_kzg_version, - keccak256, -) -from ethereum_test_tools.vm.opcode import Opcodes as Op +from ethereum_test_forks import Fork +from ethereum_test_tools import AccessList, Account, Address, Alloc, Bytecode, Environment, Hash +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import Storage, Transaction, add_kzg_version, keccak256 from .spec import Spec, SpecHelpers @@ -232,6 +222,7 @@ def tx_type() -> int: @pytest.fixture def tx( pre: Alloc, + fork: Fork, tx_to_address: Address, tx_data: bytes, tx_type: int, @@ -254,7 +245,7 @@ def tx( kwargs["access_list"] = access_list if tx_type == 3: - kwargs["max_fee_per_blob_gas"] = 1 + kwargs["max_fee_per_blob_gas"] = fork.min_base_fee_per_blob_gas() kwargs["blob_versioned_hashes"] = add_kzg_version([0], BLOB_COMMITMENT_VERSION_KZG) if tx_type > 3: diff --git a/tests/cancun/eip4844_blobs/common.py b/tests/cancun/eip4844_blobs/common.py index a97b0edf60..a10d07a0ce 100644 --- a/tests/cancun/eip4844_blobs/common.py +++ b/tests/cancun/eip4844_blobs/common.py @@ -14,7 +14,7 @@ ) from ethereum_test_tools.vm.opcode import Opcodes as Op -from .spec import Spec, SpecHelpers +from .spec import Spec INF_POINT = (0xC0 << 376).to_bytes(48, byteorder="big") Z = 0x623CE31CF9759A5C8DAF3A357992F9F3DD7F9339D8998BC8E68373E54F00B75E @@ -57,12 +57,6 @@ def blobs_to_transaction_input( return (blobs, kzg_commitments, kzg_proofs) -# Simple list of blob versioned hashes ranging from bytes32(1 to 4) -simple_blob_hashes: list[bytes] = add_kzg_version( - [(1 << x) for x in range(SpecHelpers.max_blobs_per_block())], - Spec.BLOB_COMMITMENT_VERSION_KZG, -) - # Random fixed list of blob versioned hashes random_blob_hashes = add_kzg_version( [ @@ -284,7 +278,7 @@ class BlobhashScenario: """ @staticmethod - def create_blob_hashes_list(length: int) -> list[list[bytes]]: + def create_blob_hashes_list(length: int, max_blobs_per_block: int) -> list[list[bytes]]: """ Creates a list of MAX_BLOBS_PER_BLOCK blob hashes using `random_blob_hashes`. @@ -293,20 +287,20 @@ def create_blob_hashes_list(length: int) -> list[list[bytes]]: length: MAX_BLOBS_PER_BLOCK * length -> [0x01, 0x02, 0x03, 0x04, ..., 0x0A, 0x0B, 0x0C, 0x0D] - Then split list into smaller chunks of SpecHelpers.max_blobs_per_block() + Then split list into smaller chunks of max_blobs_per_block -> [[0x01, 0x02, 0x03, 0x04], ..., [0x0a, 0x0b, 0x0c, 0x0d]] """ b_hashes = [ random_blob_hashes[i % len(random_blob_hashes)] - for i in range(SpecHelpers.max_blobs_per_block() * length) + for i in range(max_blobs_per_block * length) ] return [ - b_hashes[i : i + SpecHelpers.max_blobs_per_block()] - for i in range(0, len(b_hashes), SpecHelpers.max_blobs_per_block()) + b_hashes[i : i + max_blobs_per_block] + for i in range(0, len(b_hashes), max_blobs_per_block) ] @staticmethod - def blobhash_sstore(index: int): + def blobhash_sstore(index: int, max_blobs_per_block: int): """ Returns an BLOBHASH sstore to the given index. @@ -315,35 +309,38 @@ def blobhash_sstore(index: int): the BLOBHASH sstore. """ invalidity_check = Op.SSTORE(index, 0x01) - if index < 0 or index >= SpecHelpers.max_blobs_per_block(): + if index < 0 or index >= max_blobs_per_block: return invalidity_check + Op.SSTORE(index, Op.BLOBHASH(index)) return Op.SSTORE(index, Op.BLOBHASH(index)) @classmethod - def generate_blobhash_bytecode(cls, scenario_name: str) -> bytes: + def generate_blobhash_bytecode(cls, scenario_name: str, max_blobs_per_block: int) -> bytes: """ Returns BLOBHASH bytecode for the given scenario. """ scenarios = { "single_valid": sum( - cls.blobhash_sstore(i) for i in range(SpecHelpers.max_blobs_per_block()) + cls.blobhash_sstore(i, max_blobs_per_block) for i in range(max_blobs_per_block) ), "repeated_valid": sum( - sum(cls.blobhash_sstore(i) for _ in range(10)) - for i in range(SpecHelpers.max_blobs_per_block()) + sum(cls.blobhash_sstore(i, max_blobs_per_block) for _ in range(10)) + for i in range(max_blobs_per_block) ), "valid_invalid": sum( - cls.blobhash_sstore(i) - + cls.blobhash_sstore(SpecHelpers.max_blobs_per_block()) - + cls.blobhash_sstore(i) - for i in range(SpecHelpers.max_blobs_per_block()) + cls.blobhash_sstore(i, max_blobs_per_block) + + cls.blobhash_sstore(max_blobs_per_block, max_blobs_per_block) + + cls.blobhash_sstore(i, max_blobs_per_block) + for i in range(max_blobs_per_block) ), "varied_valid": sum( - cls.blobhash_sstore(i) + cls.blobhash_sstore(i + 1) + cls.blobhash_sstore(i) - for i in range(SpecHelpers.max_blobs_per_block() - 1) + cls.blobhash_sstore(i, max_blobs_per_block) + + cls.blobhash_sstore(i + 1, max_blobs_per_block) + + cls.blobhash_sstore(i, max_blobs_per_block) + for i in range(max_blobs_per_block - 1) ), "invalid_calls": sum( - cls.blobhash_sstore(i) for i in range(-5, SpecHelpers.max_blobs_per_block() + 5) + cls.blobhash_sstore(i, max_blobs_per_block) + for i in range(-5, max_blobs_per_block + 5) ), } scenario = scenarios.get(scenario_name) diff --git a/tests/cancun/eip4844_blobs/conftest.py b/tests/cancun/eip4844_blobs/conftest.py index e8c6c5505d..e8f3cfdfce 100644 --- a/tests/cancun/eip4844_blobs/conftest.py +++ b/tests/cancun/eip4844_blobs/conftest.py @@ -3,17 +3,268 @@ """ import pytest -from ethereum_test_tools import Alloc, Block, Hash, Transaction, add_kzg_version +from ethereum_test_forks import Fork +from ethereum_test_tools import Alloc, Block, Environment, Hash, Transaction, add_kzg_version -from .spec import BlockHeaderBlobGasFields, Spec +from .spec import Spec + + +@pytest.fixture +def block_base_fee_per_gas() -> int: + """Default max fee per gas for transactions sent during test.""" + return 7 + + +@pytest.fixture +def target_blobs_per_block(fork: Fork) -> int: + """ + Default number of blobs to be included in the block. + """ + return fork.target_blobs_per_block() + + +@pytest.fixture +def max_blobs_per_block(fork: Fork) -> int: + """ + Default number of blobs to be included in the block. + """ + return fork.max_blobs_per_block() + + +@pytest.fixture +def blob_gas_per_blob(fork: Fork) -> int: + """Default blob gas cost per blob.""" + return fork.blob_gas_per_blob() + + +@pytest.fixture(autouse=True) +def parent_excess_blobs() -> int | None: + """ + Default excess blobs of the parent block. + + Can be overloaded by a test case to provide a custom parent excess blob + count. + """ + return 10 # Defaults to a blob gas price of 1. + + +@pytest.fixture(autouse=True) +def parent_blobs() -> int | None: + """ + Default data blobs of the parent blob. + + Can be overloaded by a test case to provide a custom parent blob count. + """ + return 0 + + +@pytest.fixture +def parent_excess_blob_gas( + parent_excess_blobs: int | None, + blob_gas_per_blob: int, +) -> int | None: + """ + Calculates the excess blob gas of the parent block from the excess blobs. + """ + if parent_excess_blobs is None: + return None + assert parent_excess_blobs >= 0 + return parent_excess_blobs * blob_gas_per_blob + + +@pytest.fixture +def excess_blob_gas( + fork: Fork, + parent_excess_blobs: int | None, + parent_blobs: int | None, +) -> int | None: + """ + Calculates the excess blob gas of the block under test from the parent block. + + Value can be overloaded by a test case to provide a custom excess blob gas. + """ + if parent_excess_blobs is None or parent_blobs is None: + return None + excess_blob_gas = fork.excess_blob_gas_calculator() + return excess_blob_gas( + parent_excess_blobs=parent_excess_blobs, + parent_blob_count=parent_blobs, + ) + + +@pytest.fixture +def correct_excess_blob_gas( + fork: Fork, + parent_excess_blobs: int | None, + parent_blobs: int | None, +) -> int: + """ + Calculates the correct excess blob gas of the block under test from the parent block. + + Should not be overloaded by a test case. + """ + if parent_excess_blobs is None or parent_blobs is None: + return 0 + excess_blob_gas = fork.excess_blob_gas_calculator() + return excess_blob_gas( + parent_excess_blobs=parent_excess_blobs, + parent_blob_count=parent_blobs, + ) + + +@pytest.fixture +def block_fee_per_blob_gas( # noqa: D103 + fork: Fork, + correct_excess_blob_gas: int, +) -> int: + get_blob_gas_price = fork.blob_gas_price_calculator() + return get_blob_gas_price(excess_blob_gas=correct_excess_blob_gas) + + +@pytest.fixture +def blob_gas_price( + fork: Fork, + excess_blob_gas: int | None, +) -> int | None: + """ + Blob gas price for the block of the test. + """ + if excess_blob_gas is None: + return None + + get_blob_gas_price = fork.blob_gas_price_calculator() + return get_blob_gas_price( + excess_blob_gas=excess_blob_gas, + ) + + +@pytest.fixture +def genesis_excess_blob_gas( + parent_excess_blob_gas: int | None, + parent_blobs: int, + target_blobs_per_block: int, + blob_gas_per_blob: int, +) -> int: + """ + Default excess blob gas for the genesis block. + """ + excess_blob_gas = parent_excess_blob_gas if parent_excess_blob_gas else 0 + if parent_blobs: + # We increase the excess blob gas of the genesis because + # we cannot include blobs in the genesis, so the + # test blobs are actually in block 1. + excess_blob_gas += target_blobs_per_block * blob_gas_per_blob + return excess_blob_gas + + +@pytest.fixture +def env( + block_base_fee_per_gas: int, + genesis_excess_blob_gas: int, +) -> Environment: + """ + Prepare the environment of the genesis block for all blockchain tests. + """ + return Environment( + excess_blob_gas=genesis_excess_blob_gas, + blob_gas_used=0, + base_fee_per_gas=block_base_fee_per_gas, + ) + + +@pytest.fixture +def tx_value() -> int: + """ + Default value contained by the transactions sent during test. + + Can be overloaded by a test case to provide a custom transaction value. + """ + return 1 + + +@pytest.fixture +def tx_calldata() -> bytes: + """Default calldata in transactions sent during test.""" + return b"" + + +@pytest.fixture(autouse=True) +def tx_max_fee_per_gas( + block_base_fee_per_gas: int, +) -> int: + """ + Max fee per gas value used by all transactions sent during test. + + By default the max fee per gas is the same as the block fee per gas. + + Can be overloaded by a test case to test rejection of transactions where + the max fee per gas is insufficient. + """ + return block_base_fee_per_gas + + +@pytest.fixture +def tx_max_priority_fee_per_gas() -> int: + """ + Default max priority fee per gas for transactions sent during test. + + Can be overloaded by a test case to provide a custom max priority fee per + gas. + """ + return 0 + + +@pytest.fixture +def tx_max_fee_per_blob_gas_multiplier() -> int: + """ + Default max fee per blob gas multiplier for transactions sent during test. + + Can be overloaded by a test case to provide a custom max fee per blob gas + multiplier. + """ + return 1 + + +@pytest.fixture +def tx_max_fee_per_blob_gas_delta() -> int: + """ + Default max fee per blob gas delta for transactions sent during test. + + Can be overloaded by a test case to provide a custom max fee per blob gas + delta. + """ + return 0 + + +@pytest.fixture +def tx_max_fee_per_blob_gas( # noqa: D103 + blob_gas_price: int | None, + tx_max_fee_per_blob_gas_multiplier: int, + tx_max_fee_per_blob_gas_delta: int, +) -> int: + """ + Default max fee per blob gas for transactions sent during test. + + By default, it is set to the blob gas price of the block. + + Can be overloaded by a test case to test rejection of transactions where + the max fee per blob gas is insufficient. + """ + if blob_gas_price is None: + # When fork transitioning, the default blob gas price is 1. + return 1 + return (blob_gas_price * tx_max_fee_per_blob_gas_multiplier) + tx_max_fee_per_blob_gas_delta @pytest.fixture def non_zero_blob_gas_used_genesis_block( pre: Alloc, parent_blobs: int, + fork: Fork, + genesis_excess_blob_gas: int, parent_excess_blob_gas: int, tx_max_fee_per_gas: int, + target_blobs_per_block: int, ) -> Block | None: """ For test cases with a non-zero blobGasUsed field in the @@ -32,16 +283,19 @@ def non_zero_blob_gas_used_genesis_block( if parent_blobs == 0: return None - parent_excess_blob_gas += Spec.TARGET_BLOB_GAS_PER_BLOCK - excess_blob_gas = Spec.calc_excess_blob_gas( - BlockHeaderBlobGasFields(parent_excess_blob_gas, 0) - ) + excess_blob_gas_calculator = fork.excess_blob_gas_calculator(block_number=1) + assert parent_excess_blob_gas == excess_blob_gas_calculator( + parent_excess_blob_gas=genesis_excess_blob_gas, + parent_blob_count=0, + ), "parent excess blob gas is not as expected for extra block" sender = pre.fund_eoa(10**27) # Address that contains no code, nor balance and is not a contract. empty_account_destination = pre.fund_eoa(0) + blob_gas_price_calculator = fork.blob_gas_price_calculator(block_number=1) + return Block( txs=[ Transaction( @@ -52,7 +306,9 @@ def non_zero_blob_gas_used_genesis_block( gas_limit=21_000, max_fee_per_gas=tx_max_fee_per_gas, max_priority_fee_per_gas=0, - max_fee_per_blob_gas=Spec.get_blob_gasprice(excess_blob_gas=excess_blob_gas), + max_fee_per_blob_gas=blob_gas_price_calculator( + excess_blob_gas=parent_excess_blob_gas + ), access_list=[], blob_versioned_hashes=add_kzg_version( [Hash(x) for x in range(parent_blobs)], diff --git a/tests/cancun/eip4844_blobs/spec.py b/tests/cancun/eip4844_blobs/spec.py index c253aee0a2..0f298a39bf 100644 --- a/tests/cancun/eip4844_blobs/spec.py +++ b/tests/cancun/eip4844_blobs/spec.py @@ -1,10 +1,12 @@ """ Defines EIP-4844 specification constants and functions. """ +import itertools from dataclasses import dataclass from hashlib import sha256 -from typing import Optional +from typing import List, Optional, Tuple +from ethereum_test_forks import Fork from ethereum_test_tools import Transaction @@ -21,16 +23,6 @@ class ReferenceSpec: ref_spec_4844 = ReferenceSpec("EIPS/eip-4844.md", "f0eb6a364aaf5ccb43516fa2c269a54fb881ecfd") -@dataclass(frozen=True) -class BlockHeaderBlobGasFields: - """ - A helper class for the blob gas fields in a block header. - """ - - excess_blob_gas: int - blob_gas_used: int - - # Constants @dataclass(frozen=True) class Spec: @@ -48,17 +40,12 @@ class Spec: BLOB_COMMITMENT_VERSION_KZG = 1 POINT_EVALUATION_PRECOMPILE_ADDRESS = 10 POINT_EVALUATION_PRECOMPILE_GAS = 50_000 - MAX_BLOB_GAS_PER_BLOCK = 786432 - TARGET_BLOB_GAS_PER_BLOCK = 393216 - MIN_BLOB_GASPRICE = 1 - BLOB_GASPRICE_UPDATE_FRACTION = 3338477 # MAX_VERSIONED_HASHES_LIST_SIZE = 2**24 # MAX_CALLDATA_SIZE = 2**24 # MAX_ACCESS_LIST_SIZE = 2**24 # MAX_ACCESS_LIST_STORAGE_KEYS = 2**24 # MAX_TX_WRAP_COMMITMENTS = 2**12 # LIMIT_BLOBS_PER_TX = 2**12 - GAS_PER_BLOB = 2**17 HASH_OPCODE_BYTE = 0x49 HASH_GAS_COST = 3 @@ -80,49 +67,13 @@ def kzg_to_versioned_hash( return blob_commitment_version_kzg + sha256(kzg_commitment).digest()[1:] @classmethod - def fake_exponential(cls, factor: int, numerator: int, denominator: int) -> int: - """ - Used to calculate the blob gas cost. - """ - i = 1 - output = 0 - numerator_accumulator = factor * denominator - while numerator_accumulator > 0: - output += numerator_accumulator - numerator_accumulator = (numerator_accumulator * numerator) // (denominator * i) - i += 1 - return output // denominator - - @classmethod - def calc_excess_blob_gas(cls, parent: BlockHeaderBlobGasFields) -> int: - """ - Calculate the excess blob gas for a block given the excess blob gas - and blob gas used from the parent block header. - """ - if parent.excess_blob_gas + parent.blob_gas_used < cls.TARGET_BLOB_GAS_PER_BLOCK: - return 0 - else: - return parent.excess_blob_gas + parent.blob_gas_used - cls.TARGET_BLOB_GAS_PER_BLOCK - - @classmethod - def get_total_blob_gas(cls, tx: Transaction) -> int: + def get_total_blob_gas(cls, *, tx: Transaction, blob_gas_per_blob: int) -> int: """ Calculate the total blob gas for a transaction. """ if tx.blob_versioned_hashes is None: return 0 - return cls.GAS_PER_BLOB * len(tx.blob_versioned_hashes) - - @classmethod - def get_blob_gasprice(cls, *, excess_blob_gas: int) -> int: - """ - Calculate the blob gas price from the excess. - """ - return cls.fake_exponential( - cls.MIN_BLOB_GASPRICE, - excess_blob_gas, - cls.BLOB_GASPRICE_UPDATE_FRACTION, - ) + return blob_gas_per_blob * len(tx.blob_versioned_hashes) @dataclass(frozen=True) @@ -135,49 +86,86 @@ class SpecHelpers: BYTES_PER_FIELD_ELEMENT = 32 @classmethod - def max_blobs_per_block(cls) -> int: # MAX_BLOBS_PER_BLOCK = - """ - Returns the maximum number of blobs per block. - """ - return Spec.MAX_BLOB_GAS_PER_BLOCK // Spec.GAS_PER_BLOB - - @classmethod - def target_blobs_per_block(cls) -> int: + def get_min_excess_blob_gas_for_blob_gas_price( + cls, + *, + fork: Fork, + blob_gas_price: int, + ) -> int: """ - Returns the target number of blobs per block. + Gets the minimum required excess blob gas value to get a given blob gas cost in a block """ - return Spec.TARGET_BLOB_GAS_PER_BLOCK // Spec.GAS_PER_BLOB + current_excess_blob_gas = 0 + current_blob_gas_price = 1 + get_blob_gas_price = fork.blob_gas_price_calculator() + gas_per_blob = fork.blob_gas_per_blob() + while current_blob_gas_price < blob_gas_price: + current_excess_blob_gas += gas_per_blob + current_blob_gas_price = get_blob_gas_price(excess_blob_gas=current_excess_blob_gas) + return current_excess_blob_gas @classmethod - def calc_excess_blob_gas_from_blob_count( - cls, parent_excess_blob_gas: int, parent_blob_count: int + def get_min_excess_blobs_for_blob_gas_price( + cls, + *, + fork: Fork, + blob_gas_price: int, ) -> int: """ - Calculate the excess blob gas for a block given the parent excess blob gas - and the number of blobs in the block. + Gets the minimum required excess blobs to get a given blob gas cost in a block """ - parent_consumed_blob_gas = parent_blob_count * Spec.GAS_PER_BLOB - return Spec.calc_excess_blob_gas( - BlockHeaderBlobGasFields(parent_excess_blob_gas, parent_consumed_blob_gas) + gas_per_blob = fork.blob_gas_per_blob() + return ( + cls.get_min_excess_blob_gas_for_blob_gas_price( + fork=fork, + blob_gas_price=blob_gas_price, + ) + // gas_per_blob ) @classmethod - def get_min_excess_blob_gas_for_blob_gas_price(cls, blob_gas_price: int) -> int: + def get_blob_combinations( + cls, + blob_count: int, + ) -> List[Tuple[int, ...]]: + """ + Get all possible combinations of blobs that result in a given blob count. + """ + all = [ + seq + for i in range( + blob_count + 1, 0, -1 + ) # We can have from 1 to at most MAX_BLOBS_PER_BLOCK blobs per block + for seq in itertools.combinations_with_replacement( + range(1, blob_count + 2), i + ) # We iterate through all possible combinations + if sum(seq) == blob_count # And we only keep the ones that match the + # expected invalid blob count + ] + + # We also add the reversed version of each combination, only if it's not + # already in the list. E.g. (4, 1) is added from (1, 4) but not + # (1, 1, 1, 1, 1) because its reversed version is identical. + all += [tuple(reversed(x)) for x in all if tuple(reversed(x)) not in all] + return all + + @classmethod + def all_valid_blob_combinations(cls, fork: Fork) -> List[Tuple[int, ...]]: """ - Gets the minimum required excess blob gas value to get a given blob gas cost in a block + Returns all valid blob tx combinations for a given block, + assuming the given MAX_BLOBS_PER_BLOCK """ - current_excess_blob_gas = 0 - current_blob_gas_price = 1 - while current_blob_gas_price < blob_gas_price: - current_excess_blob_gas += Spec.GAS_PER_BLOB - current_blob_gas_price = Spec.get_blob_gasprice( - excess_blob_gas=current_excess_blob_gas - ) - return current_excess_blob_gas + max_blobs_per_block = fork.max_blobs_per_block() + all: List[Tuple[int, ...]] = [] + for i in range(1, max_blobs_per_block + 1): + all += cls.get_blob_combinations(i) + return all @classmethod - def get_min_excess_blobs_for_blob_gas_price(cls, blob_gas_price: int) -> int: + def invalid_blob_combinations(cls, fork: Fork) -> List[Tuple[int, ...]]: """ - Gets the minimum required excess blobs to get a given blob gas cost in a block + Returns invalid blob tx combinations for a given block that use up to + MAX_BLOBS_PER_BLOCK+1 blobs """ - return cls.get_min_excess_blob_gas_for_blob_gas_price(blob_gas_price) // Spec.GAS_PER_BLOB + max_blobs_per_block = fork.max_blobs_per_block() + return cls.get_blob_combinations(max_blobs_per_block + 1) diff --git a/tests/cancun/eip4844_blobs/test_blob_txs.py b/tests/cancun/eip4844_blobs/test_blob_txs.py index 6a8494bcbc..87b93188e1 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs.py @@ -15,12 +15,11 @@ """ # noqa: E501 -import itertools from typing import List, Optional, Tuple import pytest -from ethereum_test_forks import Fork, Prague +from ethereum_test_forks import Fork from ethereum_test_tools import ( EOA, AccessList, @@ -45,6 +44,7 @@ TransactionException, add_kzg_version, ) +from pytest_plugins import fork_covariant_parametrize from .spec import Spec, SpecHelpers, ref_spec_4844 @@ -77,16 +77,6 @@ def destination_account( return pre.fund_eoa(destination_account_balance) -@pytest.fixture -def tx_value() -> int: - """ - Default value contained by the transactions sent during test. - - Can be overloaded by a test case to provide a custom transaction value. - """ - return 1 - - @pytest.fixture def tx_gas( fork: Fork, @@ -98,81 +88,6 @@ def tx_gas( return tx_intrinsic_cost_calculator(calldata=tx_calldata, access_list=tx_access_list) -@pytest.fixture -def tx_calldata() -> bytes: - """Default calldata in transactions sent during test.""" - return b"" - - -@pytest.fixture -def block_fee_per_gas() -> int: - """Default max fee per gas for transactions sent during test.""" - return 7 - - -@pytest.fixture(autouse=True) -def parent_excess_blobs() -> Optional[int]: - """ - Default excess blobs of the parent block. - - Can be overloaded by a test case to provide a custom parent excess blob - count. - """ - return 10 # Defaults to a blob gas price of 1. - - -@pytest.fixture(autouse=True) -def parent_blobs() -> Optional[int]: - """ - Default data blobs of the parent blob. - - Can be overloaded by a test case to provide a custom parent blob count. - """ - return 0 - - -@pytest.fixture -def parent_excess_blob_gas( - parent_excess_blobs: Optional[int], -) -> Optional[int]: - """ - Calculates the excess blob gas of the parent block from the excess blobs. - """ - if parent_excess_blobs is None: - return None - return parent_excess_blobs * Spec.GAS_PER_BLOB - - -@pytest.fixture -def blob_gasprice( - parent_excess_blob_gas: Optional[int], - parent_blobs: Optional[int], -) -> Optional[int]: - """ - Blob gas price for the block of the test. - """ - if parent_excess_blob_gas is None or parent_blobs is None: - return None - - return Spec.get_blob_gasprice( - excess_blob_gas=SpecHelpers.calc_excess_blob_gas_from_blob_count( - parent_excess_blob_gas=parent_excess_blob_gas, - parent_blob_count=parent_blobs, - ), - ) - - -@pytest.fixture -def tx_max_priority_fee_per_gas() -> int: - """ - Default max priority fee per gas for transactions sent during test. - - Can be overloaded by a test case to provide a custom max priority fee per - gas. - """ - return 0 - - @pytest.fixture def blobs_per_tx() -> List[int]: """ @@ -206,6 +121,7 @@ def blob_hashes_per_tx(blobs_per_tx: List[int]) -> List[List[bytes]]: @pytest.fixture def total_account_minimum_balance( # noqa: D103 + blob_gas_per_blob: int, tx_gas: int, tx_value: int, tx_max_fee_per_gas: int, @@ -218,7 +134,7 @@ def total_account_minimum_balance( # noqa: D103 """ minimum_cost = 0 for tx_blob_count in [len(x) for x in blob_hashes_per_tx]: - blob_cost = tx_max_fee_per_blob_gas * Spec.GAS_PER_BLOB * tx_blob_count + blob_cost = tx_max_fee_per_blob_gas * blob_gas_per_blob * tx_blob_count minimum_cost += (tx_gas * tx_max_fee_per_gas) + tx_value + blob_cost return minimum_cost @@ -227,8 +143,9 @@ def total_account_minimum_balance( # noqa: D103 def total_account_transactions_fee( # noqa: D103 tx_gas: int, tx_value: int, - blob_gasprice: int, - block_fee_per_gas: int, + blob_gas_price: int, + block_base_fee_per_gas: int, + blob_gas_per_blob: int, tx_max_fee_per_gas: int, tx_max_priority_fee_per_gas: int, blob_hashes_per_tx: List[List[bytes]], @@ -238,47 +155,16 @@ def total_account_transactions_fee( # noqa: D103 """ total_cost = 0 for tx_blob_count in [len(x) for x in blob_hashes_per_tx]: - blob_cost = blob_gasprice * Spec.GAS_PER_BLOB * tx_blob_count + blob_cost = blob_gas_price * blob_gas_per_blob * tx_blob_count block_producer_fee = ( - tx_max_fee_per_gas - block_fee_per_gas if tx_max_priority_fee_per_gas else 0 + tx_max_fee_per_gas - block_base_fee_per_gas if tx_max_priority_fee_per_gas else 0 + ) + total_cost += ( + (tx_gas * (block_base_fee_per_gas + block_producer_fee)) + tx_value + blob_cost ) - total_cost += (tx_gas * (block_fee_per_gas + block_producer_fee)) + tx_value + blob_cost return total_cost -@pytest.fixture(autouse=True) -def tx_max_fee_per_gas( - block_fee_per_gas: int, -) -> int: - """ - Max fee per gas value used by all transactions sent during test. - - By default the max fee per gas is the same as the block fee per gas. - - Can be overloaded by a test case to test rejection of transactions where - the max fee per gas is insufficient. - """ - return block_fee_per_gas - - -@pytest.fixture -def tx_max_fee_per_blob_gas( # noqa: D103 - blob_gasprice: Optional[int], -) -> int: - """ - Default max fee per blob gas for transactions sent during test. - - By default, it is set to the blob gas price of the block. - - Can be overloaded by a test case to test rejection of transactions where - the max fee per blob gas is insufficient. - """ - if blob_gasprice is None: - # When fork transitioning, the default blob gas price is 1. - return 1 - return blob_gasprice - - @pytest.fixture def tx_access_list() -> List[AccessList]: """ @@ -290,24 +176,14 @@ def tx_access_list() -> List[AccessList]: @pytest.fixture -def tx_error( - request: pytest.FixtureRequest, - fork: Fork, -) -> Optional[TransactionException]: +def tx_error() -> Optional[TransactionException]: """ Default expected error produced by the block transactions (no error). Can be overloaded on test cases where the transactions are expected to fail. """ - if not hasattr(request, "param"): - return None - if fork >= Prague and request.param in [ - TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, - TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED, - ]: - return None - return request.param + return None @pytest.fixture @@ -367,30 +243,9 @@ def account_balance_modifier() -> int: return 0 -@pytest.fixture -def env( - parent_excess_blob_gas: Optional[int], - parent_blobs: int, -) -> Environment: - """ - Prepare the environment of the genesis block for all blockchain tests. - """ - excess_blob_gas = parent_excess_blob_gas if parent_excess_blob_gas else 0 - if parent_blobs: - # We increase the excess blob gas of the genesis because - # we cannot include blobs in the genesis, so the - # test blobs are actually in block 1. - excess_blob_gas += Spec.TARGET_BLOB_GAS_PER_BLOCK - return Environment( - excess_blob_gas=excess_blob_gas, - blob_gas_used=0, - ) - - @pytest.fixture def state_env( - parent_excess_blob_gas: Optional[int], - parent_blobs: int, + excess_blob_gas: Optional[int], ) -> Environment: """ Prepare the environment for all state test cases. @@ -400,10 +255,7 @@ def state_env( is not decreased by the target. """ return Environment( - excess_blob_gas=SpecHelpers.calc_excess_blob_gas_from_blob_count( - parent_excess_blob_gas=parent_excess_blob_gas if parent_excess_blob_gas else 0, - parent_blob_count=parent_blobs, - ), + excess_blob_gas=excess_blob_gas if excess_blob_gas else 0, ) @@ -459,13 +311,17 @@ def expected_blob_gas_used( block_number=block_number, timestamp=block_timestamp ): return Header.EMPTY_FIELD - return sum([Spec.get_total_blob_gas(tx) for tx in txs]) + blob_gas_per_blob = fork.blob_gas_per_blob( + block_number=block_number, + timestamp=block_timestamp, + ) + return sum([Spec.get_total_blob_gas(tx=tx, blob_gas_per_blob=blob_gas_per_blob) for tx in txs]) @pytest.fixture def expected_excess_blob_gas( fork: Fork, - parent_excess_blob_gas: Optional[int], + parent_excess_blobs: Optional[int], parent_blobs: Optional[int], block_number: int, block_timestamp: int, @@ -477,8 +333,9 @@ def expected_excess_blob_gas( block_number=block_number, timestamp=block_timestamp ): return Header.EMPTY_FIELD - return SpecHelpers.calc_excess_blob_gas_from_blob_count( - parent_excess_blob_gas=parent_excess_blob_gas if parent_excess_blob_gas else 0, + excess_blob_gas = fork.excess_blob_gas_calculator() + return excess_blob_gas( + parent_excess_blobs=parent_excess_blobs if parent_excess_blobs else 0, parent_blob_count=parent_blobs if parent_blobs else 0, ) @@ -534,56 +391,9 @@ def block( ) -def all_valid_blob_combinations() -> List[Tuple[int, ...]]: - """ - Returns all valid blob tx combinations for a given block, - assuming the given MAX_BLOBS_PER_BLOCK - """ - all = [ - seq - for i in range( - SpecHelpers.max_blobs_per_block(), 0, -1 - ) # We can have from 1 to at most MAX_BLOBS_PER_BLOCK blobs per block - for seq in itertools.combinations_with_replacement( - range(1, SpecHelpers.max_blobs_per_block() + 1), i - ) # We iterate through all possible combinations - if sum(seq) - <= SpecHelpers.max_blobs_per_block() # And we only keep the ones that are valid - ] - # We also add the reversed version of each combination, only if it's not - # already in the list. E.g. (2, 1, 1) is added from (1, 1, 2) but not - # (1, 1, 1) because its reversed version is identical. - all += [tuple(reversed(x)) for x in all if tuple(reversed(x)) not in all] - return all - - -def invalid_blob_combinations() -> List[Tuple[int, ...]]: - """ - Returns invalid blob tx combinations for a given block that use up to - MAX_BLOBS_PER_BLOCK+1 blobs - """ - all = [ - seq - for i in range( - SpecHelpers.max_blobs_per_block() + 1, 0, -1 - ) # We can have from 1 to at most MAX_BLOBS_PER_BLOCK blobs per block - for seq in itertools.combinations_with_replacement( - range(1, SpecHelpers.max_blobs_per_block() + 2), i - ) # We iterate through all possible combinations - if sum(seq) - == SpecHelpers.max_blobs_per_block() + 1 # And we only keep the ones that match the - # expected invalid blob count - ] - # We also add the reversed version of each combination, only if it's not - # already in the list. E.g. (4, 1) is added from (1, 4) but not - # (1, 1, 1, 1, 1) because its reversed version is identical. - all += [tuple(reversed(x)) for x in all if tuple(reversed(x)) not in all] - return all - - -@pytest.mark.parametrize( - "blobs_per_tx", - all_valid_blob_combinations(), +@fork_covariant_parametrize( + parameter_names=["blobs_per_tx"], + fn=SpecHelpers.all_valid_blob_combinations, ) @pytest.mark.valid_from("Cancun") def test_valid_blob_tx_combinations( @@ -611,26 +421,70 @@ def test_valid_blob_tx_combinations( ) -@pytest.mark.parametrize( - "parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", - [ - # tx max_blob_gas_cost of the transaction is not enough +def generate_invalid_tx_max_fee_per_blob_gas_tests( + fork: Fork, +) -> List: + """ + Returns a list of tests for invalid blob transactions due to insufficient max fee per blob gas + parametrized for each different fork. + """ + min_base_fee_per_blob_gas = fork.min_base_fee_per_blob_gas() + minimum_excess_blobs_for_first_increment = SpecHelpers.get_min_excess_blobs_for_blob_gas_price( + fork=fork, + blob_gas_price=min_base_fee_per_blob_gas + 1, + ) + next_base_fee_per_blob_gas = fork.blob_gas_price_calculator()( + excess_blob_gas=minimum_excess_blobs_for_first_increment, + ) + + tests = [] + tests.append( pytest.param( - SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) - 1, # blob gas price is 1 - SpecHelpers.target_blobs_per_block() + 1, # blob gas cost increases to 2 - 1, # tx max_blob_gas_cost is 1 + minimum_excess_blobs_for_first_increment - 1, # blob gas price is 1 + fork.target_blobs_per_block() + 1, # blob gas cost increases to above the minimum + min_base_fee_per_blob_gas, # tx max_blob_gas_cost is the minimum TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, id="insufficient_max_fee_per_blob_gas", - ), - # tx max_blob_gas_cost of the transaction is zero, which is invalid + ) + ) + if (next_base_fee_per_blob_gas - min_base_fee_per_blob_gas) > 1: + tests.append( + pytest.param( + minimum_excess_blobs_for_first_increment + - 1, # blob gas price is one less than the minimum + fork.target_blobs_per_block() + 1, # blob gas cost increases to above the minimum + next_base_fee_per_blob_gas + - 1, # tx max_blob_gas_cost is one less than the minimum + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + id="insufficient_max_fee_per_blob_gas_one_less_than_next", + ) + ) + if min_base_fee_per_blob_gas > 1: + tests.append( + pytest.param( + 0, # blob gas price is the minimum + 0, # blob gas cost stays put at 1 + min_base_fee_per_blob_gas - 1, # tx max_blob_gas_cost is one less than the minimum + TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + id="insufficient_max_fee_per_blob_gas_one_less_than_min", + ) + ) + + tests.append( pytest.param( - 0, # blob gas price is 1 + 0, # blob gas price is the minimum 0, # blob gas cost stays put at 1 0, # tx max_blob_gas_cost is 0 TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, id="invalid_max_fee_per_blob_gas", - ), - ], + ) + ) + return tests + + +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", + fn=generate_invalid_tx_max_fee_per_blob_gas_tests, ) @pytest.mark.parametrize( "account_balance_modifier", @@ -662,26 +516,9 @@ def test_invalid_tx_max_fee_per_blob_gas( ) -@pytest.mark.parametrize( - "parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", - [ - # tx max_blob_gas_cost of the transaction is not enough - pytest.param( - SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) - 1, # blob gas price is 1 - SpecHelpers.target_blobs_per_block() + 1, # blob gas cost increases to 2 - 1, # tx max_blob_gas_cost is 1 - TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, - id="insufficient_max_fee_per_blob_gas", - ), - # tx max_blob_gas_cost of the transaction is zero, which is invalid - pytest.param( - 0, # blob gas price is 1 - 0, # blob gas cost stays put at 1 - 0, # tx max_blob_gas_cost is 0 - TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, - id="invalid_max_fee_per_blob_gas", - ), - ], +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs,parent_blobs,tx_max_fee_per_blob_gas,tx_error", + fn=generate_invalid_tx_max_fee_per_blob_gas_tests, ) @pytest.mark.valid_from("Cancun") def test_invalid_tx_max_fee_per_blob_gas_state( @@ -741,15 +578,12 @@ def test_invalid_normal_gas( ) -@pytest.mark.parametrize( - "blobs_per_tx", - invalid_blob_combinations(), +@fork_covariant_parametrize( + parameter_names="blobs_per_tx", + fn=SpecHelpers.invalid_blob_combinations, ) @pytest.mark.parametrize( - "tx_error", - [TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED], - ids=[""], - indirect=True, + "tx_error", [TransactionException.TYPE_3_TX_MAX_BLOB_GAS_ALLOWANCE_EXCEEDED], ids=[""] ) @pytest.mark.valid_from("Cancun") def test_invalid_block_blob_count( @@ -787,7 +621,7 @@ def test_invalid_block_blob_count( [b"", b"\x00", b"\x01"], ids=["no_calldata", "single_zero_calldata", "single_one_calldata"], ) -@pytest.mark.parametrize("tx_max_fee_per_blob_gas", [1, 100, 10000]) +@pytest.mark.parametrize("tx_max_fee_per_blob_gas_multiplier", [1, 100, 10000]) @pytest.mark.parametrize("account_balance_modifier", [-1], ids=["exact_balance_minus_1"]) @pytest.mark.parametrize("tx_error", [TransactionException.INSUFFICIENT_ACCOUNT_FUNDS], ids=[""]) @pytest.mark.valid_from("Cancun") @@ -816,6 +650,13 @@ def test_insufficient_balance_blob_tx( ) +@fork_covariant_parametrize( + parameter_names="blobs_per_tx", + fn=lambda fork: [ + pytest.param([1], id="single_blob"), + pytest.param([fork.max_blobs_per_block()], id="max_blobs"), + ], +) @pytest.mark.parametrize( "tx_access_list", [[], [AccessList(address=100, storage_keys=[100, 200])]], @@ -829,7 +670,7 @@ def test_insufficient_balance_blob_tx( [b"", b"\x00", b"\x01"], ids=["no_calldata", "single_zero_calldata", "single_one_calldata"], ) -@pytest.mark.parametrize("tx_max_fee_per_blob_gas", [1, 100, 10000]) +@pytest.mark.parametrize("tx_max_fee_per_blob_gas_multiplier", [1, 100, 10000]) @pytest.mark.valid_from("Cancun") def test_sufficient_balance_blob_tx( state_test: StateTestFiller, @@ -856,6 +697,13 @@ def test_sufficient_balance_blob_tx( ) +@fork_covariant_parametrize( + parameter_names="blobs_per_tx", + fn=lambda fork: [ + pytest.param([1], id="single_blob"), + pytest.param([fork.max_blobs_per_block()], id="max_blobs"), + ], +) @pytest.mark.parametrize( "tx_access_list", [[], [AccessList(address=100, storage_keys=[100, 200])]], @@ -869,7 +717,7 @@ def test_sufficient_balance_blob_tx( [b"", b"\x00", b"\x01"], ids=["no_calldata", "single_zero_calldata", "single_one_calldata"], ) -@pytest.mark.parametrize("tx_max_fee_per_blob_gas", [1, 100, 10000]) +@pytest.mark.parametrize("tx_max_fee_per_blob_gas_multiplier", [1, 100, 10000]) @pytest.mark.parametrize("sender_initial_balance", [0]) @pytest.mark.valid_from("Cancun") def test_sufficient_balance_blob_tx_pre_fund_tx( @@ -914,6 +762,13 @@ def test_sufficient_balance_blob_tx_pre_fund_tx( ) +@fork_covariant_parametrize( + parameter_names="blobs_per_tx", + fn=lambda fork: [ + pytest.param([1], id="single_blob"), + pytest.param([fork.max_blobs_per_block()], id="max_blobs"), + ], +) @pytest.mark.parametrize( "tx_access_list", [[], [AccessList(address=100, storage_keys=[100, 200])]], @@ -927,7 +782,7 @@ def test_sufficient_balance_blob_tx_pre_fund_tx( [b"", b"\x01"], ids=["no_calldata", "single_non_zero_byte_calldata"], ) -@pytest.mark.parametrize("tx_max_fee_per_blob_gas", [1, 100]) +@pytest.mark.parametrize("tx_max_fee_per_blob_gas_multiplier", [1, 100]) @pytest.mark.parametrize( "tx_gas", [500_000], ids=[""] ) # Increase gas to account for contract code @@ -983,9 +838,9 @@ def test_blob_gas_subtraction_tx( ) -@pytest.mark.parametrize( - "blobs_per_tx", - all_valid_blob_combinations(), +@fork_covariant_parametrize( + parameter_names="blobs_per_tx", + fn=SpecHelpers.all_valid_blob_combinations, ) @pytest.mark.parametrize("account_balance_modifier", [-1], ids=["exact_balance_minus_1"]) @pytest.mark.parametrize("tx_error", [TransactionException.INSUFFICIENT_ACCOUNT_FUNDS], ids=[""]) @@ -1010,17 +865,29 @@ def test_insufficient_balance_blob_tx_combinations( ) -@pytest.mark.parametrize( - "blobs_per_tx,tx_error", - [ - ([0], TransactionException.TYPE_3_TX_ZERO_BLOBS), - ( - [SpecHelpers.max_blobs_per_block() + 1], +def generate_invalid_tx_blob_count_tests( + fork: Fork, +) -> List: + """ + Returns a list of tests for invalid blob transactions due to invalid blob counts. + """ + return [ + pytest.param( + [0], + TransactionException.TYPE_3_TX_ZERO_BLOBS, + id="too_few_blobs", + ), + pytest.param( + [fork.max_blobs_per_block() + 1], TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED, + id="too_many_blobs", ), - ], - ids=["too_few_blobs", "too_many_blobs"], - indirect=["tx_error"], + ] + + +@fork_covariant_parametrize( + parameter_names="blobs_per_tx,tx_error", + fn=generate_invalid_tx_blob_count_tests, ) @pytest.mark.valid_from("Cancun") def test_invalid_tx_blob_count( @@ -1183,9 +1050,9 @@ def test_invalid_blob_tx_contract_creation( ) -# ---------------------------------------- -# Opcode Tests in Blob Transaction Context -# ---------------------------------------- +# # ---------------------------------------- +# # Opcode Tests in Blob Transaction Context +# # ---------------------------------------- @pytest.fixture @@ -1193,7 +1060,7 @@ def opcode( request, sender: EOA, tx_calldata: bytes, - block_fee_per_gas: int, + block_base_fee_per_gas: int, tx_max_fee_per_gas: int, tx_max_priority_fee_per_gas: int, tx_value: int, @@ -1232,12 +1099,12 @@ def opcode( {0: tx_calldata.ljust(32, b"\x00")}, ) elif request.param == Op.GASPRICE: - assert tx_max_fee_per_gas >= block_fee_per_gas + assert tx_max_fee_per_gas >= block_base_fee_per_gas return ( Op.SSTORE(0, Op.GASPRICE), { - 0: min(tx_max_priority_fee_per_gas, tx_max_fee_per_gas - block_fee_per_gas) - + block_fee_per_gas + 0: min(tx_max_priority_fee_per_gas, tx_max_fee_per_gas - block_base_fee_per_gas) + + block_base_fee_per_gas }, ) raise Exception("Unknown opcode") @@ -1421,8 +1288,8 @@ def test_blob_tx_attribute_calldata_opcodes( @pytest.mark.parametrize("tx_max_priority_fee_per_gas", [0, 2]) # always below data fee -@pytest.mark.parametrize("tx_max_fee_per_blob_gas", [1, 3]) # normal and above priority fee -@pytest.mark.parametrize("tx_max_fee_per_gas", [100]) # always above priority fee +@pytest.mark.parametrize("tx_max_fee_per_blob_gas_delta", [0, 1]) # normal and above priority fee +@pytest.mark.parametrize("tx_max_fee_per_gas", [100]) # always above priority fee (FOR CANCUN) @pytest.mark.parametrize("opcode", [Op.GASPRICE], indirect=True) @pytest.mark.parametrize("tx_gas", [500_000]) @pytest.mark.valid_from("Cancun") diff --git a/tests/cancun/eip4844_blobs/test_blob_txs_full.py b/tests/cancun/eip4844_blobs/test_blob_txs_full.py index 0f27179c99..69ba9188b3 100644 --- a/tests/cancun/eip4844_blobs/test_blob_txs_full.py +++ b/tests/cancun/eip4844_blobs/test_blob_txs_full.py @@ -7,6 +7,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Address, Alloc, @@ -18,6 +19,7 @@ Transaction, TransactionException, ) +from pytest_plugins import fork_covariant_parametrize from .common import INF_POINT, Blob from .spec import Spec, SpecHelpers, ref_spec_4844 @@ -54,12 +56,6 @@ def tx_calldata() -> bytes: return b"" -@pytest.fixture -def block_fee_per_gas() -> int: - """Default max fee per gas for transactions sent during test.""" - return 7 - - @pytest.fixture(autouse=True) def parent_excess_blobs() -> int: """ @@ -81,32 +77,6 @@ def parent_blobs() -> int: return 0 -@pytest.fixture -def parent_excess_blob_gas( - parent_excess_blobs: int, -) -> int: - """ - Calculates the excess blob gas of the parent block from the excess blobs. - """ - return parent_excess_blobs * Spec.GAS_PER_BLOB - - -@pytest.fixture -def blob_gasprice( - parent_excess_blob_gas: int, - parent_blobs: int, -) -> int: - """ - Blob gas price for the block of the test. - """ - return Spec.get_blob_gasprice( - excess_blob_gas=SpecHelpers.calc_excess_blob_gas_from_blob_count( - parent_excess_blob_gas=parent_excess_blob_gas, - parent_blob_count=parent_blobs, - ), - ) - - @pytest.fixture def tx_max_priority_fee_per_gas() -> int: """ @@ -128,7 +98,7 @@ def txs_versioned_hashes(txs_blobs: List[List[Blob]]) -> List[List[bytes]]: @pytest.fixture(autouse=True) def tx_max_fee_per_gas( - block_fee_per_gas: int, + block_base_fee_per_gas: int, ) -> int: """ Max fee per gas value used by all transactions sent during test. @@ -138,12 +108,12 @@ def tx_max_fee_per_gas( Can be overloaded by a test case to test rejection of transactions where the max fee per gas is insufficient. """ - return block_fee_per_gas + return block_base_fee_per_gas @pytest.fixture def tx_max_fee_per_blob_gas( # noqa: D103 - blob_gasprice: Optional[int], + blob_gas_price: Optional[int], ) -> int: """ Default max fee per blob gas for transactions sent during test. @@ -153,10 +123,10 @@ def tx_max_fee_per_blob_gas( # noqa: D103 Can be overloaded by a test case to test rejection of transactions where the max fee per blob gas is insufficient. """ - if blob_gasprice is None: + if blob_gas_price is None: # When fork transitioning, the default blob gas price is 1. return 1 - return blob_gasprice + return blob_gas_price @pytest.fixture @@ -236,6 +206,7 @@ def env( def blocks( txs: List[Transaction], txs_wrapped_blobs: List[bool], + blob_gas_per_blob: int, ) -> List[Block]: """ Prepare the list of blocks for all test cases. @@ -258,7 +229,7 @@ def blocks( if tx.blob_versioned_hashes is not None ] ) - * Spec.GAS_PER_BLOB + * blob_gas_per_blob ) return [ Block( @@ -267,59 +238,63 @@ def blocks( ] -@pytest.mark.parametrize( - "txs_blobs,txs_wrapped_blobs", - [ - ( +def generate_full_blob_tests( + fork: Fork, +) -> List: + """ + Returns a list of tests for invalid blob transactions due to insufficient max fee per blob gas + parametrized for each different fork. + """ + blob_size = Spec.FIELD_ELEMENTS_PER_BLOB * SpecHelpers.BYTES_PER_FIELD_ELEMENT + max_blobs = fork.max_blobs_per_block() + return [ + pytest.param( [ # Txs [ # Blobs per transaction Blob( - blob=bytes( - Spec.FIELD_ELEMENTS_PER_BLOB * SpecHelpers.BYTES_PER_FIELD_ELEMENT - ), + blob=bytes(blob_size), kzg_commitment=INF_POINT, kzg_proof=INF_POINT, ), ] ], [True], + id="one_full_blob_one_tx", ), - ( + pytest.param( [ # Txs [ # Blobs per transaction Blob( - blob=bytes( - Spec.FIELD_ELEMENTS_PER_BLOB * SpecHelpers.BYTES_PER_FIELD_ELEMENT - ), + blob=bytes(blob_size), kzg_commitment=INF_POINT, kzg_proof=INF_POINT, ) ] - for _ in range(SpecHelpers.max_blobs_per_block()) + for _ in range(max_blobs) ], - [True] + ([False] * (SpecHelpers.max_blobs_per_block() - 1)), + [True] + ([False] * (max_blobs - 1)), + id="one_full_blob_max_txs", ), - ( + pytest.param( [ # Txs [ # Blobs per transaction Blob( - blob=bytes( - Spec.FIELD_ELEMENTS_PER_BLOB * SpecHelpers.BYTES_PER_FIELD_ELEMENT - ), + blob=bytes(blob_size), kzg_commitment=INF_POINT, kzg_proof=INF_POINT, ) ] - for _ in range(SpecHelpers.max_blobs_per_block()) + for _ in range(max_blobs) ], - ([False] * (SpecHelpers.max_blobs_per_block() - 1)) + [True], + ([False] * (max_blobs - 1)) + [True], + id="one_full_blob_at_the_end_max_txs", ), - ], - ids=[ - "one_full_blob_one_tx", - "one_full_blob_max_txs", - "one_full_blob_at_the_end_max_txs", - ], + ] + + +@fork_covariant_parametrize( + parameter_names="txs_blobs,txs_wrapped_blobs", + fn=generate_full_blob_tests, ) @pytest.mark.valid_from("Cancun") def test_reject_valid_full_blob_in_block_rlp( diff --git a/tests/cancun/eip4844_blobs/test_blobhash_opcode.py b/tests/cancun/eip4844_blobs/test_blobhash_opcode.py index f09abada60..ea91846f10 100644 --- a/tests/cancun/eip4844_blobs/test_blobhash_opcode.py +++ b/tests/cancun/eip4844_blobs/test_blobhash_opcode.py @@ -20,6 +20,7 @@ import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, Address, @@ -35,7 +36,7 @@ from ethereum_test_tools.vm.opcode import Opcodes as Op from .common import BlobhashScenario, random_blob_hashes -from .spec import Spec, SpecHelpers, ref_spec_4844 +from .spec import Spec, ref_spec_4844 REFERENCE_SPEC_GIT_PATH = ref_spec_4844.git_path REFERENCE_SPEC_VERSION = ref_spec_4844.version @@ -59,9 +60,11 @@ @pytest.mark.with_all_tx_types def test_blobhash_gas_cost( pre: Alloc, + fork: Fork, tx_type: int, blobhash_index: int, state_test: StateTestFiller, + target_blobs_per_block: int, ): """ Tests `BLOBHASH` opcode gas cost using a variety of indexes. @@ -89,8 +92,8 @@ def test_blobhash_gas_cost( access_list=[] if tx_type >= 1 else None, max_fee_per_gas=10 if tx_type >= 2 else None, max_priority_fee_per_gas=10 if tx_type >= 2 else None, - max_fee_per_blob_gas=10 if tx_type == 3 else None, - blob_versioned_hashes=random_blob_hashes[0 : SpecHelpers.target_blobs_per_block()] + max_fee_per_blob_gas=(fork.min_base_fee_per_blob_gas() * 10) if tx_type == 3 else None, + blob_versioned_hashes=random_blob_hashes[0:target_blobs_per_block] if tx_type == 3 else None, ) @@ -115,8 +118,10 @@ def test_blobhash_gas_cost( ) def test_blobhash_scenarios( pre: Alloc, + fork: Fork, scenario: str, blockchain_test: BlockchainTestFiller, + max_blobs_per_block: int, ): """ Tests that the `BLOBHASH` opcode returns the correct versioned hash for @@ -126,8 +131,12 @@ def test_blobhash_scenarios( the valid range `[0, 2**256-1]`. """ TOTAL_BLOCKS = 5 - b_hashes_list = BlobhashScenario.create_blob_hashes_list(length=TOTAL_BLOCKS) - blobhash_calls = BlobhashScenario.generate_blobhash_bytecode(scenario) + b_hashes_list = BlobhashScenario.create_blob_hashes_list( + length=TOTAL_BLOCKS, max_blobs_per_block=max_blobs_per_block + ) + blobhash_calls = BlobhashScenario.generate_blobhash_bytecode( + scenario_name=scenario, max_blobs_per_block=max_blobs_per_block + ) sender = pre.fund_eoa() blocks: List[Block] = [] @@ -146,17 +155,14 @@ def test_blobhash_scenarios( access_list=[], max_fee_per_gas=10, max_priority_fee_per_gas=10, - max_fee_per_blob_gas=10, + max_fee_per_blob_gas=(fork.min_base_fee_per_blob_gas() * 10), blob_versioned_hashes=b_hashes_list[i], ) ] ) ) post[address] = Account( - storage={ - index: b_hashes_list[i][index] - for index in range(SpecHelpers.max_blobs_per_block()) - } + storage={index: b_hashes_list[i][index] for index in range(max_blobs_per_block)} ) blockchain_test( pre=pre, @@ -173,8 +179,10 @@ def test_blobhash_scenarios( ) def test_blobhash_invalid_blob_index( pre: Alloc, + fork: Fork, blockchain_test: BlockchainTestFiller, - scenario, + scenario: str, + max_blobs_per_block: int, ): """ Tests that the `BLOBHASH` opcode returns a zeroed `bytes32` value for invalid @@ -187,13 +195,15 @@ def test_blobhash_invalid_blob_index( It confirms that the returned value is a zeroed `bytes32` for each case. """ TOTAL_BLOCKS = 5 - blobhash_calls = BlobhashScenario.generate_blobhash_bytecode(scenario) + blobhash_calls = BlobhashScenario.generate_blobhash_bytecode( + scenario_name=scenario, max_blobs_per_block=max_blobs_per_block + ) sender = pre.fund_eoa() blocks: List[Block] = [] post = {} for i in range(TOTAL_BLOCKS): address = pre.deploy_contract(blobhash_calls) - blob_per_block = (i % SpecHelpers.max_blobs_per_block()) + 1 + blob_per_block = (i % max_blobs_per_block) + 1 blobs = [random_blob_hashes[blob] for blob in range(blob_per_block)] blocks.append( Block( @@ -207,7 +217,7 @@ def test_blobhash_invalid_blob_index( access_list=[], max_fee_per_gas=10, max_priority_fee_per_gas=10, - max_fee_per_blob_gas=10, + max_fee_per_blob_gas=(fork.min_base_fee_per_blob_gas() * 10), blob_versioned_hashes=blobs, ) ] @@ -218,7 +228,7 @@ def test_blobhash_invalid_blob_index( index: (0 if index < 0 or index >= blob_per_block else blobs[index]) for index in range( -TOTAL_BLOCKS, - blob_per_block + (TOTAL_BLOCKS - (i % SpecHelpers.max_blobs_per_block())), + blob_per_block + (TOTAL_BLOCKS - (i % max_blobs_per_block)), ) } ) @@ -231,7 +241,9 @@ def test_blobhash_invalid_blob_index( def test_blobhash_multiple_txs_in_block( pre: Alloc, + fork: Fork, blockchain_test: BlockchainTestFiller, + max_blobs_per_block: int, ): """ Tests that the `BLOBHASH` opcode returns the appropriate values when there @@ -240,7 +252,9 @@ def test_blobhash_multiple_txs_in_block( Scenarios involve tx type 3 followed by tx type 2 running the same code within a block, including the opposite. """ - blobhash_bytecode = BlobhashScenario.generate_blobhash_bytecode("single_valid") + blobhash_bytecode = BlobhashScenario.generate_blobhash_bytecode( + scenario_name="single_valid", max_blobs_per_block=max_blobs_per_block + ) addresses = [pre.deploy_contract(blobhash_bytecode) for _ in range(4)] sender = pre.fund_eoa() @@ -255,10 +269,8 @@ def blob_tx(address: Address, type: int): access_list=[] if type >= 1 else None, max_fee_per_gas=10, max_priority_fee_per_gas=10, - max_fee_per_blob_gas=10 if type >= 3 else None, - blob_versioned_hashes=random_blob_hashes[0 : SpecHelpers.max_blobs_per_block()] - if type >= 3 - else None, + max_fee_per_blob_gas=(fork.min_base_fee_per_blob_gas() * 10) if type >= 3 else None, + blob_versioned_hashes=random_blob_hashes[0:max_blobs_per_block] if type >= 3 else None, ) blocks = [ @@ -283,10 +295,10 @@ def blob_tx(address: Address, type: int): ] post = { Address(address): Account( - storage={i: random_blob_hashes[i] for i in range(SpecHelpers.max_blobs_per_block())} + storage={i: random_blob_hashes[i] for i in range(max_blobs_per_block)} ) if address in (addresses[1], addresses[3]) - else Account(storage={i: 0 for i in range(SpecHelpers.max_blobs_per_block())}) + else Account(storage={i: 0 for i in range(max_blobs_per_block)}) for address in addresses } blockchain_test( diff --git a/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py b/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py index ba59dc2be8..16c1c2ccd2 100644 --- a/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py +++ b/tests/cancun/eip4844_blobs/test_blobhash_opcode_contexts.py @@ -5,8 +5,11 @@ """ # noqa: E501 +from typing import List + import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( Account, Block, @@ -15,10 +18,11 @@ TestAddress, Transaction, YulCompiler, + add_kzg_version, ) -from .common import BlobhashContext, simple_blob_hashes -from .spec import Spec, SpecHelpers, ref_spec_4844 +from .common import BlobhashContext +from .spec import Spec, ref_spec_4844 REFERENCE_SPEC_GIT_PATH = ref_spec_4844.git_path REFERENCE_SPEC_VERSION = ref_spec_4844.version @@ -26,19 +30,6 @@ pytestmark = pytest.mark.valid_from("Cancun") -# Blob transaction template -tx_type_3 = Transaction( - ty=Spec.BLOB_TX_TYPE, - data=Hash(0), - gas_limit=3000000, - max_fee_per_gas=10, - max_priority_fee_per_gas=10, - max_fee_per_blob_gas=10, - access_list=[], - blob_versioned_hashes=simple_blob_hashes, -) - - def create_opcode_context(pre, tx, post): """ Generates an opcode context based on the key provided by the @@ -51,6 +42,35 @@ def create_opcode_context(pre, tx, post): } +@pytest.fixture() +def simple_blob_hashes( + max_blobs_per_block: int, +) -> List[bytes]: + """Simple list of blob versioned hashes ranging from bytes32(1 to 4)""" + return add_kzg_version( + [(1 << x) for x in range(max_blobs_per_block)], + Spec.BLOB_COMMITMENT_VERSION_KZG, + ) + + +@pytest.fixture() +def tx_type_3( + fork: Fork, + simple_blob_hashes: List[bytes], +) -> Transaction: + """Blob transaction template.""" + return Transaction( + ty=Spec.BLOB_TX_TYPE, + data=Hash(0), + gas_limit=3000000, + max_fee_per_gas=10, + max_priority_fee_per_gas=10, + max_fee_per_blob_gas=fork.min_base_fee_per_blob_gas() * 10, + access_list=[], + blob_versioned_hashes=simple_blob_hashes, + ) + + @pytest.fixture( params=[ "on_top_level_call_stack", @@ -66,7 +86,13 @@ def create_opcode_context(pre, tx, post): "on_type_0_tx", ] ) -def opcode_context(yul: YulCompiler, request): +def opcode_context( + yul: YulCompiler, + request, + max_blobs_per_block: int, + simple_blob_hashes: List[bytes], + tx_type_3: Transaction, +): """ Fixture that is parameterized by each BLOBHASH opcode test case in order to return the corresponding constructed opcode context. @@ -137,7 +163,7 @@ def opcode_context(yul: YulCompiler, request): ), }, tx_type_3.copy( - data=Hash(0) + Hash(SpecHelpers.max_blobs_per_block() - 1), + data=Hash(0) + Hash(max_blobs_per_block - 1), to=BlobhashContext.address("delegatecall"), ), { @@ -159,7 +185,7 @@ def opcode_context(yul: YulCompiler, request): ), }, tx_type_3.copy( - data=Hash(0) + Hash(SpecHelpers.max_blobs_per_block() - 1), + data=Hash(0) + Hash(max_blobs_per_block - 1), to=BlobhashContext.address("staticcall"), ), { @@ -181,7 +207,7 @@ def opcode_context(yul: YulCompiler, request): ), }, tx_type_3.copy( - data=Hash(0) + Hash(SpecHelpers.max_blobs_per_block() - 1), + data=Hash(0) + Hash(max_blobs_per_block - 1), to=BlobhashContext.address("callcode"), ), { diff --git a/tests/cancun/eip4844_blobs/test_excess_blob_gas.py b/tests/cancun/eip4844_blobs/test_excess_blob_gas.py index 07d8bf9e95..56677de17a 100644 --- a/tests/cancun/eip4844_blobs/test_excess_blob_gas.py +++ b/tests/cancun/eip4844_blobs/test_excess_blob_gas.py @@ -22,10 +22,11 @@ """ # noqa: E501 import itertools -from typing import Dict, Iterator, List, Mapping, Optional, Tuple +from typing import Callable, Dict, Iterator, List, Mapping, Optional, Tuple import pytest +from ethereum_test_forks import Fork from ethereum_test_tools import ( EOA, Account, @@ -41,6 +42,7 @@ ) from ethereum_test_tools import Opcodes as Op from ethereum_test_tools import Transaction, add_kzg_version +from pytest_plugins import fork_covariant_parametrize from .spec import Spec, SpecHelpers, ref_spec_4844 @@ -52,27 +54,11 @@ @pytest.fixture -def parent_excess_blobs() -> int: # noqa: D103 +def parent_excess_blobs(fork: Fork) -> int: # noqa: D103 """ By default we start with an intermediate value between the target and max. """ - return (SpecHelpers.max_blobs_per_block() + SpecHelpers.target_blobs_per_block()) // 2 + 1 - - -@pytest.fixture -def parent_excess_blob_gas(parent_excess_blobs: int) -> int: # noqa: D103 - return parent_excess_blobs * Spec.GAS_PER_BLOB - - -@pytest.fixture -def correct_excess_blob_gas( # noqa: D103 - parent_excess_blob_gas: int, - parent_blobs: int, -) -> int: - return SpecHelpers.calc_excess_blob_gas_from_blob_count( - parent_excess_blob_gas=parent_excess_blob_gas, - parent_blob_count=parent_blobs, - ) + return (fork.max_blobs_per_block() + fork.target_blobs_per_block()) // 2 + 1 @pytest.fixture @@ -90,10 +76,11 @@ def header_excess_blob_gas( # noqa: D103 correct_excess_blob_gas: int, header_excess_blobs_delta: Optional[int], header_excess_blob_gas_delta: Optional[int], + blob_gas_per_blob: int, ) -> Optional[int]: if header_excess_blobs_delta is not None: modified_excess_blob_gas = correct_excess_blob_gas + ( - header_excess_blobs_delta * Spec.GAS_PER_BLOB + header_excess_blobs_delta * blob_gas_per_blob ) if modified_excess_blob_gas < 0: modified_excess_blob_gas = 2**64 + (modified_excess_blob_gas) @@ -104,58 +91,12 @@ def header_excess_blob_gas( # noqa: D103 @pytest.fixture -def block_fee_per_blob_gas( # noqa: D103 - correct_excess_blob_gas: int, -) -> int: - return Spec.get_blob_gasprice(excess_blob_gas=correct_excess_blob_gas) - - -@pytest.fixture -def block_base_fee() -> int: # noqa: D103 - return 7 - - -@pytest.fixture -def env( # noqa: D103 - parent_excess_blob_gas: int, - block_base_fee: int, - parent_blobs: int, -) -> Environment: - return Environment( - excess_blob_gas=( - parent_excess_blob_gas - if parent_blobs == 0 - else parent_excess_blob_gas + Spec.TARGET_BLOB_GAS_PER_BLOCK - ), - base_fee_per_gas=block_base_fee, - ) - - -@pytest.fixture -def tx_max_fee_per_gas( # noqa: D103 - block_base_fee: int, -) -> int: - return block_base_fee - - -@pytest.fixture -def tx_max_fee_per_blob_gas( # noqa: D103 - block_fee_per_blob_gas: int, -) -> int: - return block_fee_per_blob_gas - - -@pytest.fixture -def tx_data_cost( # noqa: D103 +def tx_blob_data_cost( # noqa: D103 tx_max_fee_per_blob_gas: int, new_blobs: int, + blob_gas_per_blob: int, ) -> int: - return tx_max_fee_per_blob_gas * Spec.GAS_PER_BLOB * new_blobs - - -@pytest.fixture -def tx_value() -> int: # noqa: D103 - return 1 + return tx_max_fee_per_blob_gas * blob_gas_per_blob * new_blobs @pytest.fixture @@ -165,9 +106,9 @@ def tx_gas_limit() -> int: # noqa: D103 @pytest.fixture def tx_exact_cost( # noqa: D103 - tx_value: int, tx_max_fee_per_gas: int, tx_data_cost: int, tx_gas_limit: int + tx_value: int, tx_max_fee_per_gas: int, tx_blob_data_cost: int, tx_gas_limit: int ) -> int: - return (tx_gas_limit * tx_max_fee_per_gas) + tx_value + tx_data_cost + return (tx_gas_limit * tx_max_fee_per_gas) + tx_value + tx_blob_data_cost @pytest.fixture @@ -191,11 +132,11 @@ def sender(pre: Alloc, tx_exact_cost: int) -> Address: # noqa: D103 @pytest.fixture def post( # noqa: D103 - destination_account: Address, tx_value: int, block_fee_per_blob_gas: int + destination_account: Address, tx_value: int, blob_gas_price: int ) -> Mapping[Address, Account]: return { destination_account: Account( - storage={0: block_fee_per_blob_gas}, + storage={0: blob_gas_price}, balance=tx_value, ), } @@ -248,8 +189,9 @@ def header_blob_gas_used() -> Optional[int]: # noqa: D103 @pytest.fixture def correct_blob_gas_used( # noqa: D103 tx: Transaction, + blob_gas_per_blob: int, ) -> int: - return Spec.get_total_blob_gas(tx) + return Spec.get_total_blob_gas(tx=tx, blob_gas_per_blob=blob_gas_per_blob) @pytest.fixture @@ -260,6 +202,8 @@ def blocks( # noqa: D103 correct_excess_blob_gas: int, correct_blob_gas_used: int, non_zero_blob_gas_used_genesis_block: Block, + max_blobs_per_block: int, + blob_gas_per_blob: int, ): blocks = ( [] @@ -292,7 +236,7 @@ def add_block( exception_message=BlockException.INCORRECT_EXCESS_BLOB_GAS, ) elif header_blob_gas_used is not None: - if header_blob_gas_used > Spec.MAX_BLOB_GAS_PER_BLOCK: + if header_blob_gas_used > (max_blobs_per_block * blob_gas_per_blob): add_block( header_modifier={"blob_gas_used": header_blob_gas_used}, exception_message=[ @@ -311,8 +255,14 @@ def add_block( return blocks -@pytest.mark.parametrize("parent_blobs", range(0, SpecHelpers.max_blobs_per_block() + 1)) -@pytest.mark.parametrize("parent_excess_blobs", range(0, SpecHelpers.target_blobs_per_block() + 1)) +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: range(0, fork.max_blobs_per_block() + 1), +) +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs", + fn=lambda fork: range(0, fork.target_blobs_per_block() + 1), +) @pytest.mark.parametrize("new_blobs", [1]) def test_correct_excess_blob_gas_calculation( blockchain_test: BlockchainTestFiller, @@ -338,26 +288,42 @@ def test_correct_excess_blob_gas_calculation( ) -BLOB_GAS_COST_INCREASES = [ - SpecHelpers.get_min_excess_blobs_for_blob_gas_price(i) - for i in [ - 2, # First blob gas cost increase - 2**32 // Spec.GAS_PER_BLOB, # Data tx wei cost 2^32 - 2**32, # blob gas cost 2^32 - 2**64 // Spec.GAS_PER_BLOB, # Data tx wei cost 2^64 - 2**64, # blob gas cost 2^64 - ( - 120_000_000 * (10**18) // Spec.GAS_PER_BLOB - ), # Data tx wei is current total Ether supply - ] -] +def generate_blob_gas_cost_increases_tests(delta: int) -> Callable[[Fork], List[int]]: + """ + Generates a list of block excess blob gas values where the blob gas price increases + based on fork properties. + """ + + def generator_function(fork: Fork) -> List[int]: + gas_per_blob = fork.blob_gas_per_blob() + return [ + SpecHelpers.get_min_excess_blobs_for_blob_gas_price( + fork=fork, blob_gas_price=blob_gas_price + ) + + delta + for blob_gas_price in [ + 2, # First blob gas cost increase + 2**32 // gas_per_blob, # Data tx wei cost 2^32 + 2**32, # blob gas cost 2^32 + 2**64 // gas_per_blob, # Data tx wei cost 2^64 + 2**64, # blob gas cost 2^64 + ( + 120_000_000 * (10**18) // gas_per_blob + ), # Data tx wei is current total Ether supply + ] + ] + return generator_function -@pytest.mark.parametrize( - "parent_excess_blobs", - [g - 1 for g in BLOB_GAS_COST_INCREASES], + +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs", + fn=generate_blob_gas_cost_increases_tests(-1), +) +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: [fork.target_blobs_per_block() + 1], ) -@pytest.mark.parametrize("parent_blobs", [SpecHelpers.target_blobs_per_block() + 1]) @pytest.mark.parametrize("new_blobs", [1]) def test_correct_increasing_blob_gas_costs( blockchain_test: BlockchainTestFiller, @@ -387,11 +353,14 @@ def test_correct_increasing_blob_gas_costs( ) -@pytest.mark.parametrize( - "parent_excess_blobs", - [g for g in BLOB_GAS_COST_INCREASES], +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs", + fn=generate_blob_gas_cost_increases_tests(0), +) +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: [fork.target_blobs_per_block() - 1], ) -@pytest.mark.parametrize("parent_blobs", [SpecHelpers.target_blobs_per_block() - 1]) @pytest.mark.parametrize("new_blobs", [1]) def test_correct_decreasing_blob_gas_costs( blockchain_test: BlockchainTestFiller, @@ -418,7 +387,10 @@ def test_correct_decreasing_blob_gas_costs( @pytest.mark.parametrize("header_excess_blob_gas", [0]) @pytest.mark.parametrize("new_blobs", [0, 1]) -@pytest.mark.parametrize("parent_blobs", range(0, SpecHelpers.max_blobs_per_block() + 1)) +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: range(0, fork.max_blobs_per_block() + 1), +) def test_invalid_zero_excess_blob_gas_in_header( blockchain_test: BlockchainTestFiller, env: Environment, @@ -452,20 +424,21 @@ def test_invalid_zero_excess_blob_gas_in_header( ) -def all_invalid_blob_gas_used_combinations() -> Iterator[Tuple[int, int]]: +def all_invalid_blob_gas_used_combinations(fork: Fork) -> Iterator[Tuple[int, int]]: """ Returns all invalid blob gas used combinations. """ - for new_blobs in range(0, SpecHelpers.max_blobs_per_block() + 1): - for header_blob_gas_used in range(0, SpecHelpers.max_blobs_per_block() + 1): + gas_per_blob = fork.blob_gas_per_blob() + for new_blobs in range(0, fork.max_blobs_per_block() + 1): + for header_blob_gas_used in range(0, fork.max_blobs_per_block() + 1): if new_blobs != header_blob_gas_used: - yield (new_blobs, header_blob_gas_used * Spec.GAS_PER_BLOB) + yield (new_blobs, header_blob_gas_used * gas_per_blob) yield (new_blobs, 2**64 - 1) -@pytest.mark.parametrize( - "new_blobs,header_blob_gas_used", - all_invalid_blob_gas_used_combinations(), +@fork_covariant_parametrize( + parameter_names="new_blobs,header_blob_gas_used", + fn=all_invalid_blob_gas_used_combinations, ) @pytest.mark.parametrize("parent_blobs", [0]) def test_invalid_blob_gas_used_in_header( @@ -475,6 +448,7 @@ def test_invalid_blob_gas_used_in_header( blocks: List[Block], new_blobs: int, header_blob_gas_used: Optional[int], + blob_gas_per_blob: int, ): """ Test rejection of blocks where the `blobGasUsed` in the header is invalid: @@ -491,20 +465,26 @@ def test_invalid_blob_gas_used_in_header( genesis_environment=env, tag="-".join( [ - f"correct:{hex(new_blobs * Spec.GAS_PER_BLOB)}", + f"correct:{hex(new_blobs * blob_gas_per_blob)}", f"header:{hex(header_blob_gas_used)}", ] ), ) -@pytest.mark.parametrize( - "header_excess_blobs_delta,parent_blobs", - [ - (-1, 0), - (+1, SpecHelpers.max_blobs_per_block()), - ], - ids=["zero_blobs_decrease_more_than_expected", "max_blobs_increase_more_than_expected"], +def generate_invalid_excess_blob_gas_above_target_change_tests(fork: Fork) -> List: + """ + Returns all invalid excess blob gas above target change tests. + """ + return [ + pytest.param(-1, 0, id="zero_blobs_decrease_more_than_expected"), + pytest.param(+1, fork.max_blobs_per_block(), id="max_blobs_increase_more_than_expected"), + ] + + +@fork_covariant_parametrize( + parameter_names="header_excess_blobs_delta,parent_blobs", + fn=generate_invalid_excess_blob_gas_above_target_change_tests, ) @pytest.mark.parametrize("new_blobs", [1]) def test_invalid_excess_blob_gas_above_target_change( @@ -541,15 +521,15 @@ def test_invalid_excess_blob_gas_above_target_change( ) -@pytest.mark.parametrize( - "parent_blobs", - [ - b - for b in range(0, SpecHelpers.max_blobs_per_block() + 1) - if b != SpecHelpers.target_blobs_per_block() +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: [ + b for b in range(0, fork.max_blobs_per_block() + 1) if b != fork.target_blobs_per_block() ], ) -@pytest.mark.parametrize("parent_excess_blobs", [1, SpecHelpers.target_blobs_per_block()]) +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs", fn=lambda fork: [1, fork.target_blobs_per_block()] +) @pytest.mark.parametrize("new_blobs", [1]) def test_invalid_static_excess_blob_gas( blockchain_test: BlockchainTestFiller, @@ -582,8 +562,14 @@ def test_invalid_static_excess_blob_gas( ) -@pytest.mark.parametrize("header_excess_blobs_delta", range(1, SpecHelpers.max_blobs_per_block())) -@pytest.mark.parametrize("parent_blobs", range(0, SpecHelpers.target_blobs_per_block() + 1)) +@fork_covariant_parametrize( + parameter_names="header_excess_blobs_delta", + fn=lambda fork: range(1, fork.max_blobs_per_block()), +) +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: range(0, fork.target_blobs_per_block() + 1), +) @pytest.mark.parametrize("parent_excess_blobs", [0]) # Start at 0 @pytest.mark.parametrize("new_blobs", [1]) def test_invalid_excess_blob_gas_target_blobs_increase_from_zero( @@ -621,9 +607,9 @@ def test_invalid_excess_blob_gas_target_blobs_increase_from_zero( @pytest.mark.parametrize("header_excess_blob_gas", [0]) -@pytest.mark.parametrize( - "parent_blobs", - range(SpecHelpers.target_blobs_per_block() + 1, SpecHelpers.max_blobs_per_block() + 1), +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: range(fork.target_blobs_per_block() + 1, fork.max_blobs_per_block() + 1), ) @pytest.mark.parametrize("parent_excess_blobs", [0]) # Start at 0 @pytest.mark.parametrize("new_blobs", [1]) @@ -661,17 +647,15 @@ def test_invalid_static_excess_blob_gas_from_zero_on_blobs_above_target( ) -@pytest.mark.parametrize( - "parent_blobs,header_excess_blobs_delta", - itertools.product( +@fork_covariant_parametrize( + parameter_names="parent_blobs,header_excess_blobs_delta", + fn=lambda fork: itertools.product( # parent_blobs - range(0, SpecHelpers.max_blobs_per_block() + 1), + range(0, fork.max_blobs_per_block() + 1), # header_excess_blobs_delta (from correct value) [ x - for x in range( - -SpecHelpers.target_blobs_per_block(), SpecHelpers.target_blobs_per_block() + 1 - ) + for x in range(-fork.target_blobs_per_block(), fork.target_blobs_per_block() + 1) if x != 0 ], ), @@ -713,13 +697,22 @@ def test_invalid_excess_blob_gas_change( ) -@pytest.mark.parametrize( - "header_excess_blob_gas", - [(2**64 + (x * Spec.GAS_PER_BLOB)) for x in range(-SpecHelpers.target_blobs_per_block(), 0)], +@fork_covariant_parametrize( + parameter_names="header_excess_blob_gas", + fn=lambda fork: [ + (2**64 + (x * fork.blob_gas_per_blob())) + for x in range(-fork.target_blobs_per_block(), 0) + ], +) +@fork_covariant_parametrize( + parameter_names="parent_blobs", + fn=lambda fork: range(fork.target_blobs_per_block()), ) -@pytest.mark.parametrize("parent_blobs", range(SpecHelpers.target_blobs_per_block())) @pytest.mark.parametrize("new_blobs", [1]) -@pytest.mark.parametrize("parent_excess_blobs", range(SpecHelpers.target_blobs_per_block())) +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs", + fn=lambda fork: range(fork.target_blobs_per_block()), +) def test_invalid_negative_excess_blob_gas( blockchain_test: BlockchainTestFiller, env: Environment, @@ -755,17 +748,20 @@ def test_invalid_negative_excess_blob_gas( ) -@pytest.mark.parametrize( - "parent_blobs,header_excess_blob_gas_delta", - [ - (SpecHelpers.target_blobs_per_block() + 1, 1), - (SpecHelpers.target_blobs_per_block() + 1, Spec.GAS_PER_BLOB - 1), - (SpecHelpers.target_blobs_per_block() - 1, -1), - (SpecHelpers.target_blobs_per_block() - 1, -(Spec.GAS_PER_BLOB - 1)), +@fork_covariant_parametrize( + parameter_names="parent_blobs,header_excess_blob_gas_delta", + fn=lambda fork: [ + (fork.target_blobs_per_block() + 1, 1), + (fork.target_blobs_per_block() + 1, fork.blob_gas_per_blob() - 1), + (fork.target_blobs_per_block() - 1, -1), + (fork.target_blobs_per_block() - 1, -(fork.blob_gas_per_blob() - 1)), ], ) @pytest.mark.parametrize("new_blobs", [1]) -@pytest.mark.parametrize("parent_excess_blobs", [SpecHelpers.target_blobs_per_block() + 1]) +@fork_covariant_parametrize( + parameter_names="parent_excess_blobs", + fn=lambda fork: [fork.target_blobs_per_block() + 1], +) def test_invalid_non_multiple_excess_blob_gas( blockchain_test: BlockchainTestFiller, env: Environment, 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 08eaa87239..e3d9b2c916 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 @@ -7,6 +7,7 @@ import pytest +from ethereum_test_forks import Cancun, Fork from ethereum_test_tools import ( Account, Address, @@ -56,12 +57,12 @@ def pre_fork_blocks(): @pytest.fixture -def post_fork_block_count() -> int: +def post_fork_block_count(fork: Fork) -> int: """ Amount of blocks to produce with the post-fork rules. """ - return SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) // ( - SpecHelpers.max_blobs_per_block() - SpecHelpers.target_blobs_per_block() + return SpecHelpers.get_min_excess_blobs_for_blob_gas_price(fork=fork, blob_gas_price=2) // ( + fork.max_blobs_per_block() - fork.target_blobs_per_block() ) @@ -225,13 +226,13 @@ def test_invalid_post_fork_block_without_blob_fields( "post_fork_block_count,blob_count_per_block", [ ( - SpecHelpers.get_min_excess_blobs_for_blob_gas_price(2) - // (SpecHelpers.max_blobs_per_block() - SpecHelpers.target_blobs_per_block()) + SpecHelpers.get_min_excess_blobs_for_blob_gas_price(fork=Cancun, blob_gas_price=2) + // (Cancun.max_blobs_per_block() - Cancun.target_blobs_per_block()) + 2, - SpecHelpers.max_blobs_per_block(), + Cancun.max_blobs_per_block(), ), (10, 0), - (10, SpecHelpers.target_blobs_per_block()), + (10, Cancun.target_blobs_per_block()), ], ids=["max_blobs", "no_blobs", "target_blobs"], ) diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py index ee819b3d71..c25839e994 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs.py @@ -2768,6 +2768,7 @@ def test_eoa_tx_after_set_code( blockchain_test: BlockchainTestFiller, pre: Alloc, tx_type: int, + fork: Fork, evm_code_type: EVMCodeType, ): """ @@ -2855,7 +2856,7 @@ def test_eoa_tx_after_set_code( value=0, max_fee_per_gas=1_000, max_priority_fee_per_gas=1_000, - max_fee_per_blob_gas=1_000, + max_fee_per_blob_gas=fork.min_base_fee_per_blob_gas() * 10, blob_versioned_hashes=add_kzg_version( [Hash(1)], Spec4844.BLOB_COMMITMENT_VERSION_KZG,