diff --git a/.github/actions/build-fixtures/action.yaml b/.github/actions/build-fixtures/action.yaml index f9f90333fb..d72bd5ccab 100644 --- a/.github/actions/build-fixtures/action.yaml +++ b/.github/actions/build-fixtures/action.yaml @@ -39,9 +39,9 @@ runs: shell: bash run: | if [ "${{ steps.evm-builder.outputs.impl }}" = "eels" ]; then - uv run fill -n ${{ steps.evm-builder.outputs.x-dist }} ${{ steps.properties.outputs.fill-params }} --output=fixtures_${{ inputs.release_name }}.tar.gz --build-name ${{ inputs.release_name }} + uv run fill -n ${{ steps.evm-builder.outputs.x-dist }} ${{ steps.properties.outputs.fill-params }} --output=fixtures_${{ inputs.release_name }}.tar.gz --build-name ${{ inputs.release_name }} --no-html --durations=100 --log-level=DEBUG else - uv run fill -n ${{ steps.evm-builder.outputs.x-dist }} --evm-bin=${{ steps.evm-builder.outputs.evm-bin }} ${{ steps.properties.outputs.fill-params }} --output=fixtures_${{ inputs.release_name }}.tar.gz --build-name ${{ inputs.release_name }} + uv run fill -n ${{ steps.evm-builder.outputs.x-dist }} --evm-bin=${{ steps.evm-builder.outputs.evm-bin }} ${{ steps.properties.outputs.fill-params }} --output=fixtures_${{ inputs.release_name }}.tar.gz --build-name ${{ inputs.release_name }} --no-html --durations=100 --log-level=DEBUG fi - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: diff --git a/.github/configs/feature.yaml b/.github/configs/feature.yaml index ca068fa85b..7dc42d5b99 100644 --- a/.github/configs/feature.yaml +++ b/.github/configs/feature.yaml @@ -1,15 +1,15 @@ # Unless filling for special features, all features should fill for previous forks (starting from Frontier) too stable: evm-type: stable - fill-params: --until=Prague --fill-static-tests --ignore=tests/static/state_tests/stQuadraticComplexityTest --no-html --durations=50 + fill-params: --until=Prague --fill-static-tests --ignore=tests/static/state_tests/stQuadraticComplexityTest develop: evm-type: develop - fill-params: --until=BPO4 --fill-static-tests --ignore=tests/static/state_tests/stQuadraticComplexityTest --no-html --durations=50 + fill-params: --until=BPO4 --fill-static-tests --ignore=tests/static/state_tests/stQuadraticComplexityTest monad: evm-type: develop # --suppress-no-test-exit-code works around a problem where multi-phase fill # (triggered by tarball output) fails to proceed on no tests processed # in 1st phase (exit code 5) - fill-params: --suppress-no-test-exit-code -m blockchain_test --from=MONAD_EIGHT --until=MONAD_NEXT --chain-id=143 -k "not eip4844 and not eip7002 and not eip7251 and not eip7685 and not eip6110 and not eip7594 and not eip7918 and not eip7610 and not eip7934 and not invalid_header" --no-html --durations=50 + fill-params: --suppress-no-test-exit-code -m blockchain_test --from=MONAD_EIGHT --until=MONAD_NEXT --chain-id=143 -k "not eip4844 and not eip7002 and not eip7251 and not eip7685 and not eip6110 and not eip7594 and not eip7918 and not eip7610 and not eip7934 and not invalid_header" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e3e702bed6..eede1e9f80 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -65,7 +65,7 @@ jobs: # TODO: tests have been updated without paying attention to filling in pre-monad # forks. Need to circle back and do proper `if fork >= ...` thing # py3: - # runs-on: ubuntu-24.04 + # runs-on: [self-hosted-ghr, size-xl-x64] # needs: static # steps: # - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 @@ -78,9 +78,17 @@ jobs: # - uses: ./.github/actions/setup-env # - name: Run py3 tests # run: tox -e py3 + # env: + # PYTEST_XDIST_AUTO_NUM_WORKERS: auto + # - name: Upload coverage reports to Codecov + # uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 + # with: + # files: .tox/coverage.xml + # flags: unittests + # token: ${{ secrets.CODECOV_TOKEN }} # pypy3: - # runs-on: ubuntu-24.04 + # runs-on: [self-hosted-ghr, size-xl-x64] # needs: static # steps: # - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66f9808075..bf183b8bce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,15 +21,79 @@ This specification aims to be: 2. **Complete** - Capture the entirety of _consensus critical_ parts of Ethereum. 3. **Accessible** - Prioritize readability, clarity, and plain language over performance and brevity. -### Spelling and Naming +### Style + +#### Spelling and Naming - Attempt to use descriptive English words (or _very common_ abbreviations) in documentation and identifiers. -- Avoid using EIP numbers in identifiers. +- Avoid using EIP numbers in identifiers, and prefer descriptive text instead (eg. `FeeMarketTransaction` instead of `Eip1559Transaction`). - If necessary, there is a custom dictionary `whitelist.txt`. +#### Comments + +- Don't repeat what is obvious from the code. +-
+ (expand) Consider how future changes will interleave with yours, especially when creating semantic blocks. + +
Consider: + + + + + + + + + + + + + + + +
Fork TFork T+1
+ + + + ```python + # EIP-1234: The dingus is the rate of fleep + dingus = a + b + dingus += c ^ d + dingus /= fleep(e) + ``` + + + + ```python + # EIP-1234: The dingus is the rate of fleep + dingus = a + b + + # EIP-4567: Frobulate the dingus + dingus = frobulate(dingus) + + dingus += c ^ d # <- + dingus /= fleep(e) # <- + ``` + +
+ + The marked lines (`<-`) are now incorrectly attributed to EIP-4567 in Fork+1. Instead, omit the EIP identifier in the comments, and describe the changes introduced by the EIP in the function's docstrings. The rendered diffs will make it pretty obvious what's changed. +
+ +#### Docstrings + +- Don't include the function's signature. +- Format using markdown. +- Don't begin with an article ("the"/"a") or a pronoun ("it", "they", etc.). +- Write in complete sentences, providing background and context for the associated code. +- Link to relevant standards/EIPs. + ### Changes across various Forks -Many contributions require changes across multiple forks, organized under `src/ethereum/*`. When making such changes, please ensure that differences between the forks are minimal and consist only of necessary differences. This will help with getting cleaner [diff outputs](https://ethereum.github.io/execution-specs/diffs/index.html). +Many contributions require changes across multiple forks, organized under `src/ethereum/forks/*`. When making such changes, please ensure that differences between the forks are minimal and consist only of necessary differences. This will help with getting cleaner [diff outputs](https://ethereum.github.io/execution-specs/diffs/index.html). When creating pull requests affecting multiple forks, we recommended submitting your PR in two steps: diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c6f5f82ba9..d14204450f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,7 @@ Test fixtures for use by clients are available for each release on the [Github r ### 🛠️ Framework - 🐞 Remove `Op.CLZ` from `UndefinedOpcodes` list ([#1970](https://github.com/ethereum/execution-specs/pull/1970)). +- 🐞 Make `TransactionTraces` `CamelModel` less lestrictive ([#2081](https://github.com/ethereum/execution-specs/pull/2081)). #### `fill` diff --git a/docs/writing_tests/test_markers.md b/docs/writing_tests/test_markers.md index 73cf74cf45..4569ba2a62 100644 --- a/docs/writing_tests/test_markers.md +++ b/docs/writing_tests/test_markers.md @@ -101,7 +101,7 @@ def type_4_default_transaction(sender: Account, pre: Alloc): @pytest.mark.with_all_typed_transactions @pytest.mark.valid_from("Prague") def test_something_with_all_tx_types( - state_test: StateTestFiller, + state_test: StateTestFiller, pre: Alloc, typed_transaction: Transaction ): @@ -327,7 +327,7 @@ In this example, the test will be marked as expected to fail when it is being ex This marker is used to mark tests that are slow to run. These tests are not run during [`tox` checks](./verifying_changes.md), and are only run when a release is being prepared. -### `@pytest.mark.pre_alloc_modify` +### `@pytest.mark.pre_alloc_mutable` This marker is used to mark tests that modify the pre-alloc in a way that would be impractical to reproduce in a real-world scenario. @@ -335,6 +335,10 @@ Examples of this include: - Modifying the pre-alloc to have a balance of 2^256 - 1. - Address collisions that would require hash collisions. +- EOA accounts containing code +- EOA accounts with a hard-coded nonce +- Contracts having zero-nonce +- Deploying a contract to a hard-coded address ### `@pytest.mark.skip()` diff --git a/packages/testing/src/execution_testing/base_types/composite_types.py b/packages/testing/src/execution_testing/base_types/composite_types.py index 1db6e9f7b2..05c05889b2 100644 --- a/packages/testing/src/execution_testing/base_types/composite_types.py +++ b/packages/testing/src/execution_testing/base_types/composite_types.py @@ -1,5 +1,7 @@ """Base composite types for Ethereum test cases.""" +import hashlib +import json from dataclasses import dataclass from typing import ( Any, @@ -367,6 +369,11 @@ class Account(CamelModel): state. """ + model_config = { + **CamelModel.model_config, + "frozen": True, + } + @dataclass(kw_only=True) class NonceMismatchError(Exception): """ @@ -514,6 +521,16 @@ def __bool__(self: "Account") -> bool: """Return True on a non-empty account.""" return any((self.nonce, self.balance, self.code, self.storage)) + def hash(self) -> Hash: + """Return the hash of the account given its properties.""" + data = self.model_dump(mode="json") + blob = json.dumps( + data, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + return Hash(hashlib.sha256(blob).digest()) + @classmethod def with_code(cls: Type, code: BytesConvertible) -> "Account": """Create account with provided `code` and nonce of `1`.""" diff --git a/packages/testing/src/execution_testing/base_types/tests/test_base_types.py b/packages/testing/src/execution_testing/base_types/tests/test_base_types.py index 3bd3551adf..49ce901868 100644 --- a/packages/testing/src/execution_testing/base_types/tests/test_base_types.py +++ b/packages/testing/src/execution_testing/base_types/tests/test_base_types.py @@ -6,7 +6,7 @@ from ..base_types import Address, Hash, Wei from ..base_types_json import to_json -from ..composite_types import AccessList +from ..composite_types import AccessList, Account @pytest.mark.parametrize( @@ -290,3 +290,57 @@ def test_json_deserialization( ) model_type = type(model_instance) assert model_type(**json) == model_instance + + +@pytest.mark.parametrize( + "account_1, account_2, equal", + [ + (Account(), Account(), True), + (Account(nonce=1), Account(nonce=2), False), + (Account(nonce=1), Account(nonce=1), True), + (Account(nonce=1), Account(nonce=1, code="0x1234"), False), + (Account(nonce=1, code="0x1234"), Account(nonce=1), False), + ( + Account(nonce=1, code="0x1234"), + Account(nonce=1, code="0x1234"), + True, + ), + ( + Account(nonce=1, code="0x1234"), + Account(nonce=1, code="0x5678"), + False, + ), + ( + Account(nonce=1, code="0x1234"), + Account(nonce=2, code="0x5678"), + False, + ), + ( + Account(nonce=1, code="0x1234"), + Account(nonce=2, code="0x1234"), + False, + ), + ( + Account(nonce=1, code="0x1234"), + Account(nonce=1, code="0x1234", storage={0: 0, 1: 1}), + False, + ), + ( + Account(nonce=1, code="0x1234", storage={1: 1, 0: 0}), + Account(nonce=1, code="0x1234", storage={0: 0, 1: 1}), + True, + ), + ], +) +def test_account_hash( + account_1: Account, account_2: Account, equal: bool +) -> None: + """Test two different accounts to return the same hash.""" + if equal: + assert account_1.hash() == account_2.hash(), ( + f"Account 1: {account_1.hash()}, Account 2: {account_2.hash()}" + ) + else: + assert account_1.hash() != account_2.hash(), ( + f"Account 1: {account_1.hash()}, Account 2: {account_2.hash()}" + ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py index 1bdde10539..73e93d0926 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/execute.py @@ -812,7 +812,7 @@ def pytest_collection_modifyitems( elif marker.name == "valid_at_transition_to": items_for_removal.append(i) continue - elif marker.name == "pre_alloc_modify": + elif marker.name == "pre_alloc_mutable": item.add_marker( pytest.mark.skip( reason="Pre-alloc modification not supported" diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py index 3ead061db1..bf39504513 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/execute/pre_alloc.py @@ -20,11 +20,9 @@ Number, Storage, StorageRootType, - ZeroPaddedHexNumber, ) from execution_testing.base_types.conversions import ( BytesConvertible, - FixedSizeBytesConvertible, NumberConvertible, ) from execution_testing.forks import Fork @@ -40,10 +38,11 @@ TransactionTestMetadata, compute_deterministic_create2_address, ) -from execution_testing.test_types import Alloc as BaseAlloc from execution_testing.tools import Initcode from execution_testing.vm import Bytecode, Op +from ..shared.pre_alloc import Alloc as SharedAlloc +from ..shared.pre_alloc import AllocFlags from .contracts import ( check_deterministic_factory_deployment, deploy_deterministic_factory_contract, @@ -226,10 +225,9 @@ class PendingTransaction(Transaction): value: HexNumber | None = None # type: ignore -class Alloc(BaseAlloc): +class Alloc(SharedAlloc): """A custom class that inherits from the original Alloc class.""" - _fork: Fork = PrivateAttr() _sender: EOA = PrivateAttr() _eth_rpc: EthRPC = PrivateAttr() _pending_txs: List[PendingTransaction] = PrivateAttr(default_factory=list) @@ -244,7 +242,6 @@ class Alloc(BaseAlloc): def __init__( self, *args: Any, - fork: Fork, sender: EOA, eth_rpc: EthRPC, eoa_iterator: Iterator[EOA], @@ -255,7 +252,6 @@ def __init__( ) -> None: """Initialize the pre-alloc with the given parameters.""" super().__init__(*args, **kwargs) - self._fork = fork self._sender = sender self._eth_rpc = eth_rpc self._eoa_iterator = eoa_iterator @@ -263,16 +259,6 @@ def __init__( self._node_id = node_id self._address_stubs = address_stubs or AddressStubs(root={}) - def __setitem__( - self, - address: Address | FixedSizeBytesConvertible, - account: Account | None, - ) -> None: - """Set account associated with an address.""" - raise ValueError( - "Tests are not allowed to set pre-alloc items in execute mode" - ) - def code_pre_processor(self, code: Bytecode) -> Bytecode: """Pre-processes the code before setting it.""" return code @@ -303,18 +289,18 @@ def _add_pending_tx( self._pending_txs.append(pending_tx) return pending_tx - def deterministic_deploy_contract( + def _deterministic_deploy_contract( self, *, deploy_code: BytesConvertible, - salt: Hash | int = 0, - initcode: BytesConvertible | None = None, - storage: Storage | StorageRootType | None = None, - label: str | None = None, + salt: Hash | int, + initcode: BytesConvertible | None, + storage: Storage | StorageRootType | None, + label: str | None, ) -> Address: """ - Deploy a contract to the allocation at a deterministic location - using a deterministic deployment proxy. + Execute implementation of contract deployment to a deterministic + location. """ del storage gas_costs = self._fork.gas_costs() @@ -410,7 +396,7 @@ def deterministic_deploy_contract( balance = self._eth_rpc.get_balance(contract_address) nonce = self._eth_rpc.get_transaction_count(contract_address) - super().__setitem__( + self.__internal_setitem__( contract_address, Account( nonce=nonce, @@ -423,18 +409,18 @@ def deterministic_deploy_contract( contract_address.label = label return contract_address - def deploy_contract( + def _deploy_contract( self, code: BytesConvertible, *, - storage: Storage | StorageRootType | None = None, - balance: NumberConvertible = 0, - nonce: NumberConvertible = 1, - address: Address | None = None, - label: str | None = None, - stub: str | None = None, + storage: Storage | StorageRootType | None, + balance: NumberConvertible, + nonce: NumberConvertible, + address: Address | None, + label: str | None, + stub: str | None, ) -> Address: - """Deploy a contract to the allocation.""" + """Execute implementation of contract deployment.""" if storage is None: storage = {} assert address is None, "address parameter is not supported" @@ -472,7 +458,7 @@ def deploy_contract( f"Stub contract {contract_address}: balance={bal_eth:.18f} " f"ETH, nonce={nonce}, code_size={len(code)} bytes" ) - super().__setitem__( + self.__internal_setitem__( contract_address, Account( nonce=nonce, @@ -560,7 +546,7 @@ def deploy_contract( "impossible to deploy contract with nonce lower than one" ) - super().__setitem__( + self.__internal_setitem__( contract_address, Account( nonce=nonce, @@ -573,19 +559,20 @@ def deploy_contract( contract_address.label = label return contract_address - def fund_eoa( + def _fund_eoa( self, - amount: NumberConvertible | None = None, - label: str | None = None, - storage: Storage | StorageRootType | None = None, - delegation: Address | Literal["Self"] | None = None, - nonce: NumberConvertible | None = None, + amount: NumberConvertible | None, + label: str | None, + storage: Storage | StorageRootType | None, + code: BytesConvertible | None, + delegation: Address | Literal["Self"] | None, + nonce: NumberConvertible | None, ) -> EOA: """ - Add a previously unused EOA to the pre-alloc with the balance specified - by `amount`. + Execute implementation of EOA funding. """ assert nonce is None, "nonce parameter is not supported for execute" + assert code is None, "code parameter is not supported for execute" eoa = next(self._eoa_iterator) eoa.label = label amount_str = ( @@ -702,7 +689,7 @@ def fund_eoa( if amount is not None: account_kwargs["balance"] = amount account = Account(**account_kwargs) - super().__setitem__(eoa, account) + self.__internal_setitem__(eoa, account) self._funded_eoa.append(eoa) balance_str = ( f"{Number(amount) / 10**18:.18f} ETH" @@ -715,41 +702,31 @@ def fund_eoa( ) return eoa - def fund_address( + def _fund_address( self, address: Address, - amount: NumberConvertible, + amount: int, *, - minimum_balance: bool = False, + minimum_balance: bool, ) -> None: """ - Fund an address with a given amount. - - If the address is already present in the pre-alloc the amount will be - added to its existing balance. + Execute implementation of address funding. """ current_balance = self._eth_rpc.get_balance(address) - fund_amount = int(Number(amount)) - if minimum_balance: - if current_balance >= fund_amount: + if current_balance >= amount: cur_eth = current_balance / 10**18 - min_eth = fund_amount / 10**18 + min_eth = amount / 10**18 logger.info( f"Skipping funding for address {address} " f"(label={address.label}): current balance " f"{cur_eth:.18f} ETH >= minimum {min_eth:.18f} ETH" ) - if address in self: - account = self[address] - if account is not None: - account.balance = ZeroPaddedHexNumber(current_balance) - else: - super().__setitem__( - address, Account(balance=current_balance) - ) + self.__internal_setitem__( + address, Account(balance=current_balance) + ) return - fund_eth = fund_amount / 10**18 + fund_eth = amount / 10**18 logger.debug( f"Funding address to minimum balance {address} " f"(label={address.label}): {fund_eth:.18f} ETH" @@ -758,11 +735,11 @@ def fund_address( action="fund_address", target=address.label, to=address, - value=fund_amount - current_balance, + value=amount - current_balance, ) - new_balance = fund_amount + new_balance = amount else: - fund_eth = fund_amount / 10**18 + fund_eth = amount / 10**18 logger.debug( f"Funding address {address} (label={address.label}): " f"{fund_eth:.18f} ETH" @@ -773,51 +750,23 @@ def fund_address( to=address, value=amount, ) - new_balance = current_balance + fund_amount + new_balance = current_balance + amount - if address in self: - account = self[address] - if account is not None: - account.balance = ZeroPaddedHexNumber(new_balance) - cur_eth = current_balance / 10**18 - new_eth = new_balance / 10**18 - logger.debug( - f"Updated balance for existing address {address}: " - f"{cur_eth:.18f} ETH -> {new_eth:.18f} ETH" - ) - else: - super().__setitem__(address, Account(balance=new_balance)) - else: - super().__setitem__(address, Account(balance=new_balance)) + self.__internal_setitem__(address, Account(balance=new_balance)) logger.info( f"Address {address} funding tx created (label={address.label}): " f"{Number(amount) / 10**18:.18f} ETH" ) - def empty_account(self) -> Address: + def _empty_account(self) -> Address: """ - Add a previously unused account guaranteed to be empty to the - pre-alloc. - - This ensures the account has: - - Zero balance - - Zero nonce - - No code - - No storage - - This is different from precompiles or system contracts. The function - does not send any transactions, ensuring that the account remains - "empty." - - Returns: - Address: The address of the created empty account. - + Execute implementation of empty account creation. """ eoa = next(self._eoa_iterator) logger.debug(f"Creating empty account at {eoa}") - super().__setitem__( + self.__internal_setitem__( eoa, Account( nonce=0, @@ -913,9 +862,27 @@ def send_pending_transactions(self) -> List[TransactionByHashResponse]: return responses +@pytest.fixture(scope="function") +def alloc_flags( + alloc_flags_from_test_markers: AllocFlags, +) -> AllocFlags: + """ + Verify this test does not require flags that are unsupported by execute. + + Otherwise skip. + """ + if AllocFlags.MUTABLE in alloc_flags_from_test_markers: + pytest.skip( + "Execute mode cannot run tests where the pre-alloction is mutated." + ) + + return alloc_flags_from_test_markers + + @pytest.fixture(autouse=True, scope="function") def pre( fork: Fork, + alloc_flags: AllocFlags, worker_key: EOA, eoa_iterator: Iterator[EOA], eth_rpc: EthRPC, @@ -941,6 +908,7 @@ def pre( ) pre = Alloc( fork=actual_fork, + flags=alloc_flags, sender=worker_key, eth_rpc=eth_rpc, eoa_iterator=eoa_iterator, diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py index 8733230a42..d4d39d1bc6 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/filler.py @@ -11,9 +11,9 @@ import datetime import gc import json +import logging import os import signal -import sys import time import warnings from dataclasses import dataclass, field @@ -30,9 +30,9 @@ from execution_testing.base_types import ( Account, Address, - Alloc, ReferenceSpec, ) +from execution_testing.base_types import Alloc as BaseAlloc from execution_testing.cli.gen_index import ( merge_partial_indexes, ) @@ -82,6 +82,7 @@ get_ref_spec_from_module, ) from .fixture_output import FixtureOutput +from .pre_alloc import Alloc # Fixture output dir for keyboard interrupt cleanup (set in pytest_configure). # Used by _merge_on_exit to merge partial JSONL files on Ctrl+C or SIGTERM. @@ -424,8 +425,8 @@ def save_pre_alloc_groups(self) -> None: def calculate_post_state_diff( - post_state: Alloc, genesis_state: Alloc -) -> Alloc: + post_state: BaseAlloc, genesis_state: BaseAlloc +) -> BaseAlloc: """ Calculate the state difference between post_state and genesis_state. @@ -471,7 +472,7 @@ def calculate_post_state_diff( # Account unchanged - don't include in diff - return Alloc(diff) + return BaseAlloc(diff) def default_output_directory() -> str: @@ -1497,24 +1498,52 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: # Get the filling session from config session: FillingSession = request.config.filling_session # type: ignore + assert isinstance(session, FillingSession) + group_salt: str | None = None + if pre_alloc_group_marker := request.node.get_closest_marker( + "pre_alloc_group" + ): + # Get the group name/salt from marker args + if pre_alloc_group_marker.args: + group_salt = str(pre_alloc_group_marker.args[0]) + else: + # We got the marker but unspecified, pass test name + group_salt = request.node.nodeid + + pre_alloc_hash: str | None = None # Phase 1: Generate pre-allocation groups if session.phase_manager.is_pre_alloc_generation: # Use the original update_pre_alloc_groups method which # returns the groups - self.update_pre_alloc_groups( - session.pre_alloc_group_builders, request.node.nodeid + assert session.pre_alloc_group_builders is not None + test_id = str(request.node.nodeid) + genesis_environment = self.get_genesis_environment() + pre_alloc_hash = pre.compute_pre_alloc_group_hash( + fork=fork, + genesis_environment=genesis_environment, + group_salt=group_salt, + ) + session.pre_alloc_group_builders.add_test_pre( + pre_alloc_hash=pre_alloc_hash, + test_id=test_id, + fork=fork, + environment=genesis_environment, + pre=pre, ) return # Skip fixture generation in phase 1 # Phase 2: Use pre-allocation groups (only for # BlockchainEngineXFixture) - pre_alloc_hash = None if ( FixtureFillingPhase.PRE_ALLOC_GENERATION in fixture_format.format_phases ): - pre_alloc_hash = self.compute_pre_alloc_group_hash() + pre_alloc_hash = pre.compute_pre_alloc_group_hash( + fork=fork, + genesis_environment=self.get_genesis_environment(), + group_salt=group_salt, + ) group = session.get_pre_alloc_group(pre_alloc_hash) self.pre = group.pre try: @@ -1785,15 +1814,21 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: - Generate index file for all produced fixtures. - Create tarball of the output directory if the output is a tarball. """ + logger = logging.getLogger("fill.sessionfinish") + is_worker = xdist.is_xdist_worker(session) + + # Workers collect logs to forward to master via workeroutput + worker_timing_logs: list[str] = [] def _log_timing(msg: str) -> None: - """Log with timestamp and flush immediately for CI visibility.""" + """Log with timestamp. Workers collect logs; master logs directly.""" log_line = f"[sessionfinish] {time.strftime('%H:%M:%S')} {msg}" - # Print to stderr (unbuffered) for immediate CI visibility - print(log_line, file=sys.stderr, flush=True) + if is_worker: + worker_timing_logs.append(log_line) + else: + logger.debug(log_line) # Log immediately when hook is entered (before any early returns) - is_worker = xdist.is_xdist_worker(session) _log_timing(f"pytest_sessionfinish ENTERED (worker={is_worker})") del exitstatus @@ -1823,6 +1858,8 @@ def _log_timing(msg: str) -> None: # waiting for other workers to finish session_instance.pre_alloc_group_builders = None gc.collect() + # Store timing logs for master to print when this worker finishes + session.config.workeroutput["timing_logs"] = worker_timing_logs # type: ignore[attr-defined] # noqa: E501 return @@ -1851,6 +1888,8 @@ def _log_timing(msg: str) -> None: fc.all_fixtures.clear() fc._fixtures_to_verify.clear() gc.collect() + # Store timing logs for master to print when this worker finishes + session.config.workeroutput["timing_logs"] = worker_timing_logs # type: ignore[attr-defined] # noqa: E501 return if fixture_output.is_stdout or is_help_or_collectonly_mode(session.config): @@ -1867,19 +1906,22 @@ def _log_timing(msg: str) -> None: ) # Remove any lock files that may have been created. - _log_timing("Removing lock files...") - t0 = time.time() - for file in fixture_output.directory.rglob("*.lock"): - file.unlink() - _log_timing(f"Lock files removed in {time.time() - t0:.1f}s") + lock_files = list(fixture_output.directory.rglob("*.lock")) + if lock_files: + _log_timing(f"Removing {len(lock_files)} lock files...") + t0 = time.time() + for file in lock_files: + file.unlink() + _log_timing(f"Lock files removed in {time.time() - t0:.1f}s") # Verify fixtures after merge if verification is enabled - _log_timing("_verify_fixtures_post_merge: starting...") - t0 = time.time() - _verify_fixtures_post_merge(session.config, fixture_output.directory) - _log_timing( - f"_verify_fixtures_post_merge: done in {time.time() - t0:.1f}s" - ) + if session.config.getoption("verify_fixtures"): + _log_timing("_verify_fixtures_post_merge: starting...") + t0 = time.time() + _verify_fixtures_post_merge(session.config, fixture_output.directory) + _log_timing( + f"_verify_fixtures_post_merge: done in {time.time() - t0:.1f}s" + ) # Generate index file for all produced fixtures by merging partial indexes. # Only merge if partial indexes were actually written (i.e., tests produced @@ -1899,9 +1941,24 @@ def _log_timing(msg: str) -> None: ) # Create tarball of the output directory if the output is a tarball. - _log_timing("create_tarball: starting...") - t0 = time.time() - fixture_output.create_tarball() - _log_timing(f"create_tarball: done in {time.time() - t0:.1f}s") + if fixture_output.is_tarball: + _log_timing("create_tarball: starting...") + t0 = time.time() + fixture_output.create_tarball() + _log_timing(f"create_tarball: done in {time.time() - t0:.1f}s") _log_timing("Finalization (master): COMPLETE") + + +def pytest_testnodedown(node: Any, error: Any) -> None: + """ + Called on master when a worker node finishes. + + Prints any timing logs collected by the worker during sessionfinish. + """ + del error + logger = logging.getLogger("fill.sessionfinish") + worker_id = getattr(node, "workerinput", {}).get("workerid", "unknown") + timing_logs = getattr(node, "workeroutput", {}).get("timing_logs", []) + for log_line in timing_logs: + logger.debug(f"[worker {worker_id}] {log_line}") diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/pre_alloc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/pre_alloc.py index dd60772f9e..75f6ee6e5d 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/pre_alloc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/pre_alloc.py @@ -1,11 +1,10 @@ """Pre-alloc specifically conditioned for test filling.""" +import hashlib import inspect -from enum import IntEnum from functools import cache from hashlib import sha256 -from itertools import count -from typing import Any, Iterator, List, Literal +from typing import Any, Dict, List, Literal import pytest from pydantic import PrivateAttr @@ -20,11 +19,9 @@ StorageRootType, TestPrivateKey, TestPrivateKey2, - ZeroPaddedHexNumber, ) from execution_testing.base_types.conversions import ( BytesConvertible, - FixedSizeBytesConvertible, NumberConvertible, ) from execution_testing.fixtures import LabeledFixtureFormat @@ -34,13 +31,15 @@ DETERMINISTIC_FACTORY_ADDRESS, DETERMINISTIC_FACTORY_BYTECODE, EOA, + Environment, compute_deterministic_create2_address, + contract_address_from_hash, + eoa_from_hash, ) -from execution_testing.test_types import Alloc as BaseAlloc from execution_testing.tools import Initcode -CONTRACT_START_ADDRESS_DEFAULT = 0x1000000000000000000000000000000000001000 -CONTRACT_ADDRESS_INCREMENTS_DEFAULT = 0x100 +from ..shared.pre_alloc import Alloc as SharedAlloc +from ..shared.pre_alloc import AllocFlags def pytest_addoption(parser: pytest.Parser) -> None: @@ -50,97 +49,112 @@ def pytest_addoption(parser: pytest.Parser) -> None: "Arguments defining pre-allocation behavior during test filling.", ) - pre_alloc_group.addoption( - "--strict-alloc", - action="store_true", - dest="strict_alloc", - default=False, - help=( - "[DEBUG ONLY] Disallows deploying a contract in a predefined " - "address." - ), - ) - pre_alloc_group.addoption( - "--ca-start", - "--contract-address-start", - action="store", - dest="test_contract_start_address", - default=f"{CONTRACT_START_ADDRESS_DEFAULT}", - type=str, - help="Starting address from which tests will deploy contracts.", - ) - pre_alloc_group.addoption( - "--ca-incr", - "--contract-address-increment", - action="store", - dest="test_contract_address_increments", - default=f"{CONTRACT_ADDRESS_INCREMENTS_DEFAULT}", - type=str, - help="Address increment value for each deployed contract by a test.", - ) - - -class AllocMode(IntEnum): - """Allocation mode for the state.""" - - PERMISSIVE = 0 - STRICT = 1 + # No options for now + del pre_alloc_group DELEGATION_DESIGNATION = b"\xef\x01\x00" +EMPTY_ACCOUNT_HASH = Account().hash() -class Alloc(BaseAlloc): +class Alloc(SharedAlloc): """Allocation of accounts in the state, pre and post test execution.""" _eoa_fund_amount_default: int = PrivateAttr(10**21) - _alloc_mode: AllocMode = PrivateAttr() - _contract_address_iterator: Iterator[Address] = PrivateAttr() - _eoa_iterator: Iterator[EOA] = PrivateAttr() - _fork: Fork = PrivateAttr() + _account_salt: Dict[Hash, int] = PrivateAttr(default_factory=dict) def __init__( - self, - *args: Any, - alloc_mode: AllocMode, - contract_address_iterator: Iterator[Address], - eoa_iterator: Iterator[EOA], - fork: Fork, - **kwargs: Any, + self, *args: Any, fork: Fork, flags: AllocFlags, **kwargs: Any ) -> None: - """Initialize allocation with the given properties.""" - super().__init__(*args, **kwargs) - self._alloc_mode = alloc_mode - self._contract_address_iterator = contract_address_iterator - self._eoa_iterator = eoa_iterator - self._fork = fork - - def __setitem__( - self, - address: Address | FixedSizeBytesConvertible, - account: Account | None, - ) -> None: - """Set account associated with an address.""" - if self._alloc_mode == AllocMode.STRICT: - raise ValueError("Cannot set items in strict mode") - super().__setitem__(address, account) + """Initialize the pre-alloc.""" + super().__init__(*args, fork=fork, flags=flags, **kwargs) + + def get_next_account_salt(self, account_hash: Hash) -> int: + """Retrieve the next salt for this account.""" + salt = self._account_salt.get(account_hash, 0) + self._account_salt[account_hash] = salt + 1 + return salt def code_pre_processor(self, code: BytesConvertible) -> BytesConvertible: """Pre-processes the code before setting it.""" return code - def deterministic_deploy_contract( + def modified_accounts_salt(self) -> int: + """ + Return a salt if this pre-allocation was affected by setting addresses + to hard-coded accounts or has pre-funded addresses. + + Any modification the test does to a hard-coded address must affect + this salt. + """ + if ( + not self._set_addresses + and not self._pre_funded_addresses + and not self._hardcoded_addresses_deployed_to + and not self._deleted_addresses + ): + return 0 + + # Build a hashable buffer from the modified accounts. + buffer = b"" + altered_accounts = ( + self._set_addresses + | self._pre_funded_addresses + | self._hardcoded_addresses_deployed_to + ) + if altered_accounts: + buffer += b"\0" + for altered_account in sorted(altered_accounts): + buffer += altered_account + account = self[altered_account] + assert account is not None + buffer += account.hash() + if self._deleted_addresses: + buffer += b"\1" + for deleted_address in sorted(self._deleted_addresses): + buffer += deleted_address + + return int.from_bytes( + hashlib.sha256(buffer).digest()[:8], byteorder="big" + ) + + def compute_pre_alloc_group_hash( + self, + *, + fork: Fork, + genesis_environment: Environment, + group_salt: str | None, + ) -> str: + """Hash (fork, env) in order to group tests by genesis config.""" + fork_digest = hashlib.sha256(fork.name().encode("utf-8")).digest() + fork_hash = int.from_bytes(fork_digest[:8], byteorder="big") + combined_hash = ( + fork_hash + ^ hash(genesis_environment) + ^ self.modified_accounts_salt() + ) + + # Check if this pre-allocation has a group salt + if group_salt: + # Add custom salt to hash + salt_hash = hashlib.sha256(group_salt.encode("utf-8")).digest() + salt_int = int.from_bytes(salt_hash[:8], byteorder="big") + combined_hash = combined_hash ^ salt_int + + return f"0x{combined_hash:016x}" + + def _deterministic_deploy_contract( self, *, deploy_code: BytesConvertible, - salt: Hash | int = 0, - initcode: BytesConvertible | None = None, - storage: Storage | StorageRootType | None = None, - label: str | None = None, + salt: Hash | int, + initcode: BytesConvertible | None, + storage: Storage | StorageRootType | None, + label: str | None, ) -> Address: """ - Deploy a contract to the allocation at a deterministic location - using a deterministic deployment proxy. + Filler implementation of contract deployment to a deterministic + location. """ if not isinstance(deploy_code, Bytes): deploy_code = Bytes(deploy_code) @@ -171,7 +185,7 @@ def deterministic_deploy_contract( fork_deterministic_factory_address is None and DETERMINISTIC_FACTORY_ADDRESS not in self ): - super().__setitem__( + self.__internal_setitem__( DETERMINISTIC_FACTORY_ADDRESS, Account( nonce=1, @@ -180,7 +194,7 @@ def deterministic_deploy_contract( ), ) - super().__setitem__( + self.__internal_setitem__( contract_address, Account( nonce=1, @@ -205,44 +219,24 @@ def deterministic_deploy_contract( contract_address.label = label return contract_address - def deploy_contract( + def _deploy_contract( self, code: BytesConvertible, *, - storage: Storage | StorageRootType | None = None, - balance: NumberConvertible = 0, - nonce: NumberConvertible = 1, - address: Address | None = None, - label: str | None = None, - stub: str | None = None, + storage: Storage | StorageRootType | None, + balance: NumberConvertible, + nonce: NumberConvertible, + address: Address | None, + label: str | None, + stub: str | None, ) -> Address: """ - Deploy a contract to the allocation. - - Warning: `address` parameter is a temporary solution to allow tests to - hard-code the contract address. Do NOT use in new tests as it will be - removed in the future! + Filler implementation of contract deployment. """ del stub if storage is None: storage = {} - if address is not None: - assert self._alloc_mode == AllocMode.PERMISSIVE, ( - "address parameter is not supported" - ) - assert address not in self, ( - f"address {address} already in allocation" - ) - contract_address = address - else: - contract_address = next(self._contract_address_iterator) - - if self._alloc_mode == AllocMode.STRICT: - assert Number(nonce) >= 1, ( - "impossible to deploy contract with nonce lower than one" - ) - code = self.code_pre_processor(code) code_bytes = ( bytes(code) if not isinstance(code, (bytes, str)) else code @@ -252,15 +246,24 @@ def deploy_contract( f"code too large: {len(code_bytes)} > {max_code_size}" ) - super().__setitem__( - contract_address, - Account( - nonce=nonce, - balance=balance, - code=code, - storage=storage, - ), + account = Account( + nonce=nonce, + balance=balance, + code=code, + storage=storage, ) + + if address is not None: + assert address not in self, ( + f"address {address} already in allocation" + ) + contract_address = address + else: + account_hash = account.hash() + salt = self.get_next_account_salt(account_hash) + contract_address = contract_address_from_hash(account_hash, salt) + + self.__internal_setitem__(contract_address, account) if label is None: # Try to deduce the label from the code frame = inspect.currentframe() @@ -278,48 +281,59 @@ def deploy_contract( contract_address.label = label return contract_address - def fund_eoa( + def _fund_eoa( self, - amount: NumberConvertible | None = None, - label: str | None = None, - storage: Storage | None = None, - delegation: Address | Literal["Self"] | None = None, - nonce: NumberConvertible | None = None, + amount: NumberConvertible | None, + label: str | None, + storage: Storage | None, + code: BytesConvertible | None, + delegation: Address | Literal["Self"] | None, + nonce: NumberConvertible | None, ) -> EOA: """ - Add a previously unused EOA to the pre-alloc with the balance specified - by `amount`. + Filler implementation of EOA funding. If amount is 0, nothing will be added to the pre-alloc but a new and unique EOA will be returned. """ del label - eoa = next(self._eoa_iterator) if amount is None: amount = self._eoa_fund_amount_default if ( Number(amount) > 0 or storage is not None + or code is not None or delegation is not None or (nonce is not None and Number(nonce) > 0) ): - if storage is None and delegation is None: + if code is not None and delegation is not None: + raise Exception( + "code and delegation cannot be set at the same time" + ) + if storage is None and delegation is None and code is None: nonce = Number(0 if nonce is None else nonce) account = Account( nonce=nonce, balance=amount, ) - if nonce > 0: - eoa.nonce = nonce else: # Type-4 transaction is sent to the EOA to set the storage, so # the nonce must be 1 - if ( - not isinstance(delegation, Address) - and delegation == "Self" - ): - delegation = eoa + if delegation is not None: + if ( + not isinstance(delegation, Address) + and delegation == "Self" + ): + # This is a placeholder value, since we don't know + # the address until the end of the function. + code = DELEGATION_DESIGNATION + b"Self" + else: + code = DELEGATION_DESIGNATION + delegation + elif code is not None: + code = Bytes(code) + else: + code = b"" # If delegation is None but storage is not, realistically the # nonce should be 2 because the account must have delegated to # set the storage and then again to reset the delegation (but @@ -330,86 +344,44 @@ def fund_eoa( nonce=nonce, balance=amount, storage=storage if storage is not None else {}, - code=DELEGATION_DESIGNATION + bytes(delegation) - if delegation is not None - else b"", + code=code, ) - eoa.nonce = nonce - super().__setitem__(eoa, account) + else: + account = Account() + + account_hash = account.hash() + salt = self.get_next_account_salt(account_hash) + eoa = eoa_from_hash(account_hash, salt) + + if account.nonce > 0: + eoa.nonce = account.nonce + + if not isinstance(delegation, Address) and delegation == "Self": + account = account.copy(code=DELEGATION_DESIGNATION + eoa) + if account: + self.__internal_setitem__(eoa, account) return eoa - def fund_address( + def _fund_address( self, address: Address, - amount: NumberConvertible, + amount: int, *, - minimum_balance: bool = False, + minimum_balance: bool, ) -> None: """ - Fund an address with a given amount. - - If the address is already present in the pre-alloc the amount will be - added to its existing balance. + Filler implementation of address funding. """ - if address in self: - account = self[address] - if account is not None: - current_balance = account.balance or 0 - fund_amount = Number(amount) - if minimum_balance: - if current_balance >= fund_amount: - return - account.balance = ZeroPaddedHexNumber(fund_amount) - else: - account.balance = ZeroPaddedHexNumber( - current_balance + fund_amount - ) - return - super().__setitem__(address, Account(balance=amount)) + del minimum_balance + self.__internal_setitem__(address, Account(balance=amount)) - def empty_account(self) -> Address: + def _empty_account(self) -> Address: """ - Add a previously unused account guaranteed to be empty to the - pre-alloc. - - This ensures the account has: - - Zero balance - - Zero nonce - - No code - - No storage - - This is different from precompiles or system contracts. The function - does not send any transactions, ensuring that the account remains - "empty." - - Returns: - Address: The address of the created empty account. - + Filler implementation of empty account creation. """ - eoa = next(self._eoa_iterator) - - return Address(eoa) - - -@pytest.fixture(scope="session") -def alloc_mode(request: pytest.FixtureRequest) -> AllocMode: - """Return allocation mode for the tests.""" - if request.config.getoption("strict_alloc"): - return AllocMode.STRICT - return AllocMode.PERMISSIVE - - -@pytest.fixture(scope="session") -def contract_start_address(request: pytest.FixtureRequest) -> int: - """Return starting address for contract deployment.""" - return int(request.config.getoption("test_contract_start_address"), 0) - - -@pytest.fixture(scope="session") -def contract_address_increments(request: pytest.FixtureRequest) -> int: - """Return address increment for contract deployment.""" - return int(request.config.getoption("test_contract_address_increments"), 0) + salt = self.get_next_account_salt(EMPTY_ACCOUNT_HASH) + return Address(eoa_from_hash(EMPTY_ACCOUNT_HASH, salt)) def sha256_from_string(s: str) -> int: @@ -467,70 +439,15 @@ def node_id_for_entropy( raise Exception(f"Fixture format name not found in test {node_id}") -@pytest.fixture(scope="function") -def contract_address_iterator( - request: pytest.FixtureRequest, - contract_start_address: int, - contract_address_increments: int, - node_id_for_entropy: str, -) -> Iterator[Address]: - """Return iterator over contract addresses with dynamic scoping.""" - if request.config.getoption( - # TODO: Ideally, we should check the fixture format instead of checking - # parameters. - "generate_pre_alloc_groups", - default=False, - ) or request.config.getoption("use_pre_alloc_groups", default=False): - # Use a starting address that is derived from the test node - contract_start_address = sha256_from_string(node_id_for_entropy) - return iter( - Address( - (contract_start_address + (i * contract_address_increments)) - % 2**160 - ) - for i in count() - ) - - @cache def eoa_by_index(i: int) -> EOA: """Return EOA by index.""" return EOA(key=TestPrivateKey + i if i != 1 else TestPrivateKey2, nonce=0) -@pytest.fixture(scope="function") -def eoa_iterator( - request: pytest.FixtureRequest, - node_id_for_entropy: str, -) -> Iterator[EOA]: - """Return iterator over EOAs copies with dynamic scoping.""" - if request.config.getoption( - # TODO: Ideally, we should check the fixture format instead of checking - # parameters. - "generate_pre_alloc_groups", - default=False, - ) or request.config.getoption("use_pre_alloc_groups", default=False): - # Use a starting address that is derived from the test node - eoa_start_pk = sha256_from_string(node_id_for_entropy) - # secp256k1 curve order constant - curve_order = ( # noqa: E501 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - ) - return iter( - EOA( - key=(eoa_start_pk + i) % curve_order, - nonce=0, - ) - for i in count() - ) - return iter(eoa_by_index(i).copy() for i in count()) - - @pytest.fixture(scope="function") def pre( - alloc_mode: AllocMode, - contract_address_iterator: Iterator[Address], - eoa_iterator: Iterator[EOA], + alloc_flags: AllocFlags, fork: Fork | None, request: pytest.FixtureRequest, ) -> Alloc: @@ -542,8 +459,6 @@ def pre( actual_fork = request.node.fork return Alloc( - alloc_mode=alloc_mode, - contract_address_iterator=contract_address_iterator, - eoa_iterator=eoa_iterator, + flags=alloc_flags, fork=actual_fork, ) diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_pre_alloc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_pre_alloc.py index 779dedb353..9bcd3f134a 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_pre_alloc.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_pre_alloc.py @@ -1,109 +1,124 @@ """Test the pre-allocation methods in the filler module.""" -from itertools import count - import pytest -from execution_testing.base_types import ( - Address, - TestPrivateKey, - TestPrivateKey2, -) +from execution_testing.base_types import Account, Address from execution_testing.forks import Fork, Prague -from execution_testing.test_types import EOA from execution_testing.vm import Op -from ..pre_alloc import ( - CONTRACT_ADDRESS_INCREMENTS_DEFAULT, - CONTRACT_START_ADDRESS_DEFAULT, - Alloc, - AllocMode, -) +from ...shared.pre_alloc import AllocFlags +from ..pre_alloc import Alloc def create_test_alloc( - alloc_mode: AllocMode = AllocMode.PERMISSIVE, + flags: AllocFlags = AllocFlags.MUTABLE, fork: Fork = Prague, ) -> Alloc: """Create a test Alloc instance with default iterators.""" - contract_iter = iter( - Address( - CONTRACT_START_ADDRESS_DEFAULT - + (i * CONTRACT_ADDRESS_INCREMENTS_DEFAULT) - ) - for i in count() - ) - eoa_iter = iter( - EOA( - key=TestPrivateKey + i if i != 1 else TestPrivateKey2, nonce=0 - ).copy() - for i in count() - ) - return Alloc( - alloc_mode=alloc_mode, - contract_address_iterator=contract_iter, - eoa_iterator=eoa_iter, + flags=flags, fork=fork, ) def test_alloc_deploy_contract_basic() -> None: """Test basic `Alloc.deploy_contract` functionality.""" - pre = create_test_alloc() + pre_1 = create_test_alloc() + pre_2 = create_test_alloc() + + contract_code_a = Op.SSTORE(0, 1) + Op.STOP + contract_code_b = Op.SSTORE(0, 2) + Op.STOP - contract_1 = pre.deploy_contract(Op.SSTORE(0, 1) + Op.STOP) - contract_2 = pre.deploy_contract(Op.SSTORE(0, 2) + Op.STOP) + contract_1_a_1 = pre_1.deploy_contract(contract_code_a) + contract_1_a_2 = pre_1.deploy_contract(contract_code_a) + contract_1_b = pre_1.deploy_contract(contract_code_b) # Contracts should be deployed to different addresses - assert contract_1 != contract_2 - assert contract_1 in pre - assert contract_2 in pre - - # Check that addresses follow expected pattern - assert contract_1 == Address(CONTRACT_START_ADDRESS_DEFAULT) - assert contract_2 == Address( - CONTRACT_START_ADDRESS_DEFAULT + CONTRACT_ADDRESS_INCREMENTS_DEFAULT - ) + assert contract_1_a_1 != contract_1_a_2 + assert contract_1_b != contract_1_a_1 + assert contract_1_b != contract_1_a_2 + assert contract_1_a_1 in pre_1 + assert contract_1_a_2 in pre_1 + assert contract_1_a_1 not in pre_2 # Check accounts exist and have code - pre_contract_1_account = pre[contract_1] - pre_contract_2_account = pre[contract_2] - assert pre_contract_1_account is not None - assert pre_contract_2_account is not None - assert pre_contract_1_account.code is not None - assert pre_contract_2_account.code is not None - assert len(pre_contract_1_account.code) > 0 - assert len(pre_contract_2_account.code) > 0 + pre_contract_1_a_1_account = pre_1[contract_1_a_1] + pre_contract_1_a_2_account = pre_1[contract_1_a_2] + assert pre_contract_1_a_1_account is not None + assert pre_contract_1_a_2_account is not None + assert pre_contract_1_a_1_account.code is not None + assert pre_contract_1_a_2_account.code is not None + assert len(pre_contract_1_a_1_account.code) > 0 + assert len(pre_contract_1_a_2_account.code) > 0 + + # Deploy contracts in second pre, verify addresses + contract_2_a_1 = pre_2.deploy_contract(contract_code_a) + contract_2_a_2 = pre_2.deploy_contract(contract_code_a) + contract_2_b = pre_2.deploy_contract(contract_code_b) + + assert contract_1_a_1 == contract_2_a_1 + assert contract_1_a_2 == contract_2_a_2 + assert contract_1_b == contract_2_b def test_alloc_deploy_contract_with_balance() -> None: """Test `Alloc.deploy_contract` with balance.""" pre = create_test_alloc() balance = 10**18 - contract = pre.deploy_contract(Op.STOP, balance=balance) - - assert contract in pre - account = pre[contract] + contract_with_balance_1 = pre.deploy_contract(Op.STOP, balance=balance) + contract_with_balance_2 = pre.deploy_contract(Op.STOP, balance=balance) + contract_without_balance = pre.deploy_contract(Op.STOP, balance=0) + assert contract_with_balance_1 != contract_without_balance + assert contract_with_balance_1 != contract_with_balance_2 + + assert contract_with_balance_1 in pre + account = pre[contract_with_balance_1] assert account is not None assert account.balance == balance + # Redeploy in another pre + pre_2 = create_test_alloc() + assert contract_with_balance_1 == pre_2.deploy_contract( + Op.STOP, balance=balance + ) + assert contract_with_balance_2 == pre_2.deploy_contract( + Op.STOP, balance=balance + ) + def test_alloc_deploy_contract_with_storage() -> None: """Test `Alloc.deploy_contract` with storage.""" pre = create_test_alloc() - storage = {0: 42, 1: 100} - contract = pre.deploy_contract( + storage_a = {0: 42, 1: 100} + contract_with_storage_1 = pre.deploy_contract( Op.STOP, - storage=storage, # type: ignore + storage=storage_a, # type: ignore ) + contract_with_storage_2 = pre.deploy_contract( + Op.STOP, + storage=storage_a, # type: ignore + ) + contract_without_storage = pre.deploy_contract(Op.STOP, storage={}) + assert contract_with_storage_1 != contract_without_storage + assert contract_with_storage_1 != contract_with_storage_2 - assert contract in pre - account = pre[contract] + assert contract_with_storage_1 in pre + account = pre[contract_with_storage_1] assert account is not None assert account.storage is not None - assert account.storage[0] == 42 - assert account.storage[1] == 100 + assert account.storage[0] == storage_a[0] + assert account.storage[1] == storage_a[1] + + # Redeploy in another pre + pre_2 = create_test_alloc() + assert contract_with_storage_1 == pre_2.deploy_contract( + Op.STOP, + storage=storage_a, # type: ignore + ) + assert contract_with_storage_2 == pre_2.deploy_contract( + Op.STOP, + storage=storage_a, # type: ignore + ) def test_alloc_fund_eoa_basic() -> None: @@ -127,20 +142,6 @@ def test_alloc_fund_eoa_basic() -> None: assert account_2.balance == 2 * 10**18 -def test_alloc_fund_address() -> None: - """Test `Alloc.fund_address` functionality.""" - pre = create_test_alloc() - address = Address(0x1234567890123456789012345678901234567890) - amount = 5 * 10**18 - - pre.fund_address(address, amount) - - assert address in pre - account = pre[address] - assert account is not None - assert account.balance == amount - - def test_alloc_empty_account() -> None: """Test `Alloc.empty_account` functionality.""" pre = create_test_alloc() @@ -166,42 +167,116 @@ def test_alloc_deploy_contract_code_types() -> None: assert account.code == bytes.fromhex("600160005500") -@pytest.mark.parametrize( - "alloc_mode", [AllocMode.STRICT, AllocMode.PERMISSIVE] -) -def test_alloc_modes(alloc_mode: AllocMode) -> None: +@pytest.mark.parametrize("flags", [AllocFlags.NONE, AllocFlags.MUTABLE]) +def test_alloc_flags(flags: AllocFlags) -> None: """Test different allocation modes.""" - pre = create_test_alloc(alloc_mode=alloc_mode) + pre = create_test_alloc(flags=flags) - assert pre._alloc_mode == alloc_mode + assert pre._flags == flags # Test that we can deploy contracts regardless of mode contract = pre.deploy_contract(Op.STOP) assert contract in pre +def test_alloc_flag_allow_account_address_set() -> None: + """ + Test that setting a hard-coded address to an account only works + when mutable. + """ + # With flag: should allow setting accounts directly + pre_with_flag = create_test_alloc(flags=AllocFlags.MUTABLE) + address = Address(0x1234567890123456789012345678901234567890) + + pre_with_flag[address] = Account(balance=100) + assert address in pre_with_flag + + # Without flag: should raise + pre_without_flag = create_test_alloc(flags=AllocFlags.NONE) + with pytest.raises(ValueError, match="Cannot set items in immutable mode"): + pre_without_flag[address] = Account(balance=100) + + +def test_alloc_flag_allow_deploy_to_hardcoded_address() -> None: + """Test that deploying to hardcoded addresses requires MUTABLE flag.""" + # With flag: should allow deploying to hardcoded address + pre_with_flag = create_test_alloc(flags=AllocFlags.MUTABLE) + hardcoded_address = Address(0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF) + contract = pre_with_flag.deploy_contract( + Op.STOP, address=hardcoded_address + ) + assert contract == hardcoded_address + assert contract in pre_with_flag + + # Without flag: should raise + pre_without_flag = create_test_alloc(flags=AllocFlags.NONE) + with pytest.raises(ValueError, match="Cannot set items in immutable mode"): + pre_without_flag.deploy_contract(Op.STOP, address=hardcoded_address) + + +def test_alloc_flag_allow_zero_nonce_contracts() -> None: + """Test that deploying contracts with zero nonce requires MUTABLE flag.""" + # With flag: should allow deploying contracts with nonce 0 + pre_with_flag = create_test_alloc(flags=AllocFlags.MUTABLE) + contract = pre_with_flag.deploy_contract(Op.STOP, nonce=0) + assert contract in pre_with_flag + account = pre_with_flag[contract] + assert account is not None + assert account.nonce == 0 + + # Without flag: should raise + pre_without_flag = create_test_alloc(flags=AllocFlags.NONE) + with pytest.raises(ValueError, match="Cannot set items in immutable mode"): + pre_without_flag.deploy_contract(Op.STOP, nonce=0) + + +def test_alloc_mutable_flag_combines_permissions() -> None: + """Test that MUTABLE flag includes multiple permissions.""" + pre = create_test_alloc(flags=AllocFlags.MUTABLE) + + # Should allow account address set + address = Address(0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) + + pre[address] = Account(balance=100) + assert address in pre + + # Should allow deploying to hardcoded address + hardcoded_address = Address(0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB) + contract = pre.deploy_contract(Op.STOP, address=hardcoded_address) + assert contract == hardcoded_address + + # Should allow zero nonce contracts + zero_nonce_contract = pre.deploy_contract(Op.STOP, nonce=0) + assert zero_nonce_contract in pre + account = pre[zero_nonce_contract] + assert account is not None + assert account.nonce == 0 + + def test_global_address_allocation_consistency() -> None: """Test that address allocation produces consistent results.""" # Create two alloc instances with same parameters - pre1 = create_test_alloc() - pre2 = create_test_alloc() + pre_1 = create_test_alloc() + pre_2 = create_test_alloc() # Deploy contracts and check they get the same addresses - contract1_pre1 = pre1.deploy_contract(Op.STOP) - contract1_pre2 = pre2.deploy_contract(Op.STOP) + contract_1_pre_1 = pre_1.deploy_contract(Op.STOP) + contract_1_pre_2 = pre_2.deploy_contract(Op.STOP) # Should get same starting address - assert contract1_pre1 == contract1_pre2 - assert contract1_pre1 == Address(CONTRACT_START_ADDRESS_DEFAULT) + assert contract_1_pre_1 == contract_1_pre_2 # Second contracts should also match - contract2_pre1 = pre1.deploy_contract(Op.STOP) - contract2_pre2 = pre2.deploy_contract(Op.STOP) + contract_2_pre_1 = pre_1.deploy_contract(Op.STOP) + contract_2_pre_2 = pre_2.deploy_contract(Op.STOP) - assert contract2_pre1 == contract2_pre2 - assert contract2_pre1 == Address( - CONTRACT_START_ADDRESS_DEFAULT + CONTRACT_ADDRESS_INCREMENTS_DEFAULT - ) + assert contract_2_pre_1 == contract_2_pre_2 + + # Third contract, when distinct, should not match + contract_3_pre_1 = pre_1.deploy_contract(Op.INVALID) + contract_3_pre_2 = pre_2.deploy_contract(Op.STOP) + + assert contract_3_pre_1 != contract_3_pre_2 def test_alloc_deploy_contract_nonce() -> None: @@ -219,7 +294,7 @@ def test_alloc_fund_eoa_returns_eoa_object() -> None: """Test that fund_eoa returns proper EOA object with private key access.""" pre = create_test_alloc() - eoa = pre.fund_eoa(10**18) + eoa = pre.fund_eoa() # Should be able to access private key (EOA object) assert hasattr(eoa, "key") @@ -229,23 +304,4 @@ def test_alloc_fund_eoa_returns_eoa_object() -> None: assert eoa in pre account = pre[eoa] assert account is not None - assert account.balance == 10**18 - - -def test_alloc_multiple_contracts_sequential_addresses() -> None: - """Test that multiple contracts get sequential addresses.""" - pre = create_test_alloc() - - contracts = [] - for i in range(5): - contract = pre.deploy_contract(Op.PUSH1(i) + Op.STOP) - contracts.append(contract) - - # Check addresses are sequential - for i, contract in enumerate(contracts): - expected_addr = Address( - CONTRACT_START_ADDRESS_DEFAULT - + (i * CONTRACT_ADDRESS_INCREMENTS_DEFAULT) - ) - assert contract == expected_addr - assert contract in pre + assert account.balance == pre._eoa_fund_amount_default diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_prealloc_group.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_prealloc_group.py index 650f8fb2a2..e9f8ec6663 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_prealloc_group.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_prealloc_group.py @@ -7,12 +7,16 @@ import pytest +from execution_testing.base_types import Address from execution_testing.fixtures import BaseFixture, PreAllocGroups from execution_testing.forks import Fork, Prague from execution_testing.specs.base import BaseTest -from execution_testing.test_types import Alloc, Environment +from execution_testing.test_types import Environment +from execution_testing.vm import Op +from ...shared.pre_alloc import AllocFlags from ..filler import default_output_directory +from ..pre_alloc import Alloc class MockTest(BaseTest): @@ -45,16 +49,46 @@ def get_genesis_environment(self) -> Environment: return self.genesis_environment.set_fork_requirements(self.fork) +def test_pre_alloc_group_same() -> None: + """Test that pre_alloc_group("separate") forces unique grouping.""" + # Create mock environment and pre-allocation + env = Environment() + pre_1 = Alloc(fork=Prague, flags=AllocFlags.NONE) + pre_2 = Alloc(fork=Prague, flags=AllocFlags.NONE) + + # Deploy different contracts and fund eoas with different amounts, + # should still result in the same group hash. + pre_1.deploy_contract(code=Op.STOP) + pre_2.deploy_contract(code=Op.INVALID) + + pre_1.fund_eoa(amount=0) + pre_2.fund_eoa(amount=1) + + # Create test without marker + hash1 = pre_1.compute_pre_alloc_group_hash( + fork=Prague, genesis_environment=env, group_salt=None + ) + hash2 = pre_1.compute_pre_alloc_group_hash( + fork=Prague, genesis_environment=env, group_salt=None + ) + + # Hashes should be equal + assert hash1 == hash2 + + def test_pre_alloc_group_separate() -> None: """Test that pre_alloc_group("separate") forces unique grouping.""" # Create mock environment and pre-allocation env = Environment() - pre = Alloc() + pre = Alloc(fork=Prague, flags=AllocFlags.NONE) fork = Prague # Create test without marker test1 = MockTest(pre=pre, genesis_environment=env, fork=fork) - hash1 = test1.compute_pre_alloc_group_hash() + genesis_env1 = test1.get_genesis_environment() + hash1 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env1, group_salt=None + ) # Create test with "separate" marker mock_request = Mock() @@ -67,14 +101,23 @@ def test_pre_alloc_group_separate() -> None: test2 = MockTest( pre=pre, genesis_environment=env, request=mock_request, fork=fork ) - hash2 = test2.compute_pre_alloc_group_hash() + genesis_env2 = test2.get_genesis_environment() + # For "separate" marker, use the node ID as the salt + hash2 = pre.compute_pre_alloc_group_hash( + fork=fork, + genesis_environment=genesis_env2, + group_salt=mock_request.node.nodeid, + ) # Hashes should be different due to "separate" marker assert hash1 != hash2 # Create another test without marker - should match first test test3 = MockTest(pre=pre, genesis_environment=env, fork=fork) - hash3 = test3.compute_pre_alloc_group_hash() + genesis_env3 = test3.get_genesis_environment() + hash3 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env3, group_salt=None + ) assert hash1 == hash3 @@ -82,7 +125,7 @@ def test_pre_alloc_group_separate() -> None: def test_pre_alloc_group_custom_salt() -> None: """Test that custom group names create consistent grouping.""" env = Environment() - pre = Alloc() + pre = Alloc(fork=Prague, flags=AllocFlags.NONE) fork = Prague # Create test with custom group "eip1234" @@ -96,7 +139,10 @@ def test_pre_alloc_group_custom_salt() -> None: test1 = MockTest( pre=pre, genesis_environment=env, request=mock_request1, fork=fork ) - hash1 = test1.compute_pre_alloc_group_hash() + genesis_env1 = test1.get_genesis_environment() + hash1 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env1, group_salt="eip1234" + ) # Create another test with same custom group "eip1234" mock_request2 = Mock() @@ -111,7 +157,10 @@ def test_pre_alloc_group_custom_salt() -> None: test2 = MockTest( pre=pre, genesis_environment=env, request=mock_request2, fork=fork ) - hash2 = test2.compute_pre_alloc_group_hash() + genesis_env2 = test2.get_genesis_environment() + hash2 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env2, group_salt="eip1234" + ) # Hashes should be the same - both in "eip1234" group assert hash1 == hash2 @@ -127,7 +176,10 @@ def test_pre_alloc_group_custom_salt() -> None: test3 = MockTest( pre=pre, genesis_environment=env, request=mock_request3, fork=fork ) - hash3 = test3.compute_pre_alloc_group_hash() + genesis_env3 = test3.get_genesis_environment() + hash3 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env3, group_salt="eip5678" + ) # Hash should be different - different custom group assert hash1 != hash3 @@ -137,7 +189,7 @@ def test_pre_alloc_group_custom_salt() -> None: def test_pre_alloc_group_separate_different_nodeids() -> None: """Test that different tests with "separate" get different hashes.""" env = Environment() - pre = Alloc() + pre = Alloc(fork=Prague, flags=AllocFlags.NONE) fork = Prague # Create test with "separate" and nodeid1 @@ -151,7 +203,12 @@ def test_pre_alloc_group_separate_different_nodeids() -> None: test1 = MockTest( pre=pre, genesis_environment=env, request=mock_request1, fork=fork ) - hash1 = test1.compute_pre_alloc_group_hash() + genesis_env1 = test1.get_genesis_environment() + hash1 = pre.compute_pre_alloc_group_hash( + fork=fork, + genesis_environment=genesis_env1, + group_salt=mock_request1.node.nodeid, + ) # Create test with "separate" and nodeid2 mock_request2 = Mock() @@ -164,7 +221,12 @@ def test_pre_alloc_group_separate_different_nodeids() -> None: test2 = MockTest( pre=pre, genesis_environment=env, request=mock_request2, fork=fork ) - hash2 = test2.compute_pre_alloc_group_hash() + genesis_env2 = test2.get_genesis_environment() + hash2 = pre.compute_pre_alloc_group_hash( + fork=fork, + genesis_environment=genesis_env2, + group_salt=mock_request2.node.nodeid, + ) # Hashes should be different due to different nodeids assert hash1 != hash2 @@ -173,7 +235,7 @@ def test_pre_alloc_group_separate_different_nodeids() -> None: def test_no_pre_alloc_group_marker() -> None: """Test normal grouping without pre_alloc_group marker.""" env = Environment() - pre = Alloc() + pre = Alloc(fork=Prague, flags=AllocFlags.NONE) fork = Prague # Create test without marker but with request object @@ -185,11 +247,17 @@ def test_no_pre_alloc_group_marker() -> None: test1 = MockTest( pre=pre, genesis_environment=env, request=mock_request, fork=fork ) - hash1 = test1.compute_pre_alloc_group_hash() + genesis_env1 = test1.get_genesis_environment() + hash1 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env1, group_salt=None + ) # Create test without any request test2 = MockTest(pre=pre, genesis_environment=env, fork=fork) - hash2 = test2.compute_pre_alloc_group_hash() + genesis_env2 = test2.get_genesis_environment() + hash2 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env2, group_salt=None + ) # Hashes should be the same - both have no marker assert hash1 == hash2 @@ -198,7 +266,7 @@ def test_no_pre_alloc_group_marker() -> None: def test_pre_alloc_group_with_reason() -> None: """Test that reason kwarg is accepted but doesn't affect grouping.""" env = Environment() - pre = Alloc() + pre = Alloc(fork=Prague, flags=AllocFlags.NONE) fork = Prague # Create test with custom group and reason @@ -215,7 +283,12 @@ def test_pre_alloc_group_with_reason() -> None: test1 = MockTest( pre=pre, genesis_environment=env, request=mock_request1, fork=fork ) - hash1 = test1.compute_pre_alloc_group_hash() + genesis_env1 = test1.get_genesis_environment() + hash1 = pre.compute_pre_alloc_group_hash( + fork=fork, + genesis_environment=genesis_env1, + group_salt="hardcoded_addresses", + ) # Create another test with same group but different reason mock_request2 = Mock() @@ -229,12 +302,109 @@ def test_pre_alloc_group_with_reason() -> None: test2 = MockTest( pre=pre, genesis_environment=env, request=mock_request2, fork=fork ) - hash2 = test2.compute_pre_alloc_group_hash() + genesis_env2 = test2.get_genesis_environment() + hash2 = pre.compute_pre_alloc_group_hash( + fork=fork, + genesis_environment=genesis_env2, + group_salt="hardcoded_addresses", + ) # Hashes should be the same - reason doesn't affect grouping assert hash1 == hash2 +def test_pre_alloc_group_with_modified_alloc() -> None: + """Test that modifications to Alloc affect grouping via group_salt().""" + env = Environment() + fork = Prague + + # Create unmodified pre-allocation + pre1 = Alloc(fork=fork, flags=AllocFlags.NONE) + test1 = MockTest(pre=pre1, genesis_environment=env, fork=fork) + genesis_env1 = test1.get_genesis_environment() + hash1 = pre1.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env1, group_salt=None + ) + + # Create pre-allocation with a funded address + pre2 = Alloc(fork=fork, flags=AllocFlags.NONE) + pre2.fund_address( + Address("0x1234567890123456789012345678901234567890"), amount=100 + ) + test2 = MockTest(pre=pre2, genesis_environment=env, fork=fork) + genesis_env2 = test2.get_genesis_environment() + hash2 = pre2.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env2, group_salt=None + ) + + # Hashes should be different - pre2 has modifications + assert hash1 != hash2 + + +def test_pre_alloc_explicit_salt_overrides_group_salt() -> None: + """ + Test that explicit group_salt parameter overrides group_salt() method. + """ + env = Environment() + fork = Prague + + # Create pre-allocation with modifications + pre = Alloc(fork=fork, flags=AllocFlags.NONE) + pre.fund_address( + Address("0x1234567890123456789012345678901234567890"), amount=100 + ) + + test1 = MockTest(pre=pre, genesis_environment=env, fork=fork) + genesis_env1 = test1.get_genesis_environment() + + # Hash with explicit salt should ignore the internal group_salt() + hash1 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env1, group_salt="custom_salt" + ) + + # Hash without explicit salt uses internal group_salt() + hash2 = pre.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env1, group_salt=None + ) + + # Hashes should be different + assert hash1 != hash2 + + +def test_pre_alloc_group_same_modifications() -> None: + """Test that identical modifications produce the same hash.""" + env = Environment() + fork = Prague + + # Create two pre-allocations with same modification + pre1 = Alloc(fork=fork, flags=AllocFlags.NONE) + pre1.fund_address( + Address("0x1234567890123456789012345678901234567890"), amount=100 + ) + + pre2 = Alloc(fork=fork, flags=AllocFlags.NONE) + pre2.fund_address( + Address("0x1234567890123456789012345678901234567890"), amount=100 + ) + + test1 = MockTest(pre=pre1, genesis_environment=env, fork=fork) + test2 = MockTest(pre=pre2, genesis_environment=env, fork=fork) + + genesis_env1 = test1.get_genesis_environment() + genesis_env2 = test2.get_genesis_environment() + + hash1 = pre1.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env1, group_salt=None + ) + + hash2 = pre2.compute_pre_alloc_group_hash( + fork=fork, genesis_environment=genesis_env2, group_salt=None + ) + + # Hashes should be the same - both have identical modifications + assert hash1 == hash2 + + class FormattedTest: """Represents a single formatted test.""" diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py index b059420838..bd572fae28 100644 --- a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/execute_fill.py @@ -15,6 +15,7 @@ from execution_testing.specs.base import OpMode from execution_testing.test_types import EOA, Alloc, ChainConfig +from ..shared.pre_alloc import AllocFlags from ..spec_version_checker.spec_version_checker import EIPSpecTestItem ALL_FIXTURE_PARAMETERS = { @@ -157,11 +158,6 @@ def pytest_configure(config: pytest.Config) -> None: "pre_alloc_group: Control shared pre-allocation grouping (use " '"separate" for isolated group or custom string for named groups)', ) - config.addinivalue_line( - "markers", - "pre_alloc_modify: Marks a test to apply plugin-specific " - "pre_alloc_group modifiers", - ) config.addinivalue_line( "markers", "slow: Marks a test as slow (deselect with '-m \"not slow\"')", @@ -183,6 +179,11 @@ def pytest_configure(config: pytest.Config) -> None: "markers", "fully_tagged: Marks a static test as fully tagged with all metadata.", ) + config.addinivalue_line( + "markers", + "pre_alloc_mutable: Marks a test to allow impossible mutations in the " + "pre-state.", + ) @pytest.fixture(scope="function") @@ -270,6 +271,30 @@ def chain_config() -> ChainConfig: return ChainConfig() +@pytest.fixture(scope="function") +def alloc_flags_from_test_markers( + request: pytest.FixtureRequest, +) -> AllocFlags: + """Return allocation mode for a given test based on its markers.""" + flags = AllocFlags.NONE + if request.node.get_closest_marker("pre_alloc_mutable"): + flags |= AllocFlags.MUTABLE + return flags + + +@pytest.fixture(scope="function") +def alloc_flags( + alloc_flags_from_test_markers: AllocFlags, +) -> AllocFlags: + """ + Return allocation mode for the test. + + By default, this is based on markers tests only, but plugins can + override this behavior. + """ + return alloc_flags_from_test_markers + + def pytest_addoption(parser: pytest.Parser) -> None: """Add command-line options to pytest.""" static_filler_group = parser.getgroup( diff --git a/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/pre_alloc.py b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/pre_alloc.py new file mode 100644 index 0000000000..09bb2b311c --- /dev/null +++ b/packages/testing/src/execution_testing/cli/pytest_commands/plugins/shared/pre_alloc.py @@ -0,0 +1,335 @@ +"""Shared pre-alloc functionality.""" + +from enum import IntFlag, auto +from typing import Any, Literal, Set + +from pydantic import PrivateAttr + +from execution_testing.base_types import ( + Account, + Address, + Hash, + Number, + Storage, + StorageRootType, +) +from execution_testing.base_types.conversions import ( + BytesConvertible, + FixedSizeBytesConvertible, + NumberConvertible, +) +from execution_testing.forks import Fork +from execution_testing.test_types import EOA +from execution_testing.test_types import Alloc as BaseAlloc + + +class AllocFlags(IntFlag): + """Feature flags for allocation behavior.""" + + NONE = 0 + MUTABLE = auto() + + +class Alloc(BaseAlloc): + """ + Allocation subclass that enforces rules set by the allocation flags. + """ + + _fork: Fork = PrivateAttr() + _flags: AllocFlags = PrivateAttr(AllocFlags.NONE) + _set_addresses: Set[Address] = PrivateAttr(default_factory=set) + _deleted_addresses: Set[Address] = PrivateAttr(default_factory=set) + _pre_funded_addresses: Set[Address] = PrivateAttr(default_factory=set) + _hardcoded_addresses_deployed_to: Set[Address] = PrivateAttr( + default_factory=set + ) + + def is_mutable(self) -> bool: + """Return whether the pre-alloc is mutable.""" + return bool(self._flags & AllocFlags.MUTABLE) + + def assert_mutable(self) -> None: + """Raises an exception if the MUTABLE flag is not set.""" + if not self.is_mutable(): + raise ValueError( + "Cannot set items in immutable mode. " + "Use `pytest.mark.pre_alloc_mutable` to allow mutable mode." + ) + return + + def __init__( + self, + *args: Any, + fork: Fork, + flags: AllocFlags, + **kwargs: Any, + ) -> None: + """Initialize allocation with the given properties.""" + super().__init__(*args, **kwargs) + self._fork = fork + self._flags = flags + + def __setitem__( + self, + address: Address | FixedSizeBytesConvertible, + account: Account | None, + ) -> None: + """Set account associated with an address.""" + self.assert_mutable() + if not isinstance(address, Address): + address = Address(address) + self._set_addresses.add(address) + self.__internal_setitem__(address, account) + + def __internal_setitem__( + self, + address: Address, + account: Account | None, + ) -> None: + """ + Set account associated with an address. + + Called by the pre-alloc implementation to set an account. + """ + self.root[address] = account + + def __delitem__( + self, address: Address | FixedSizeBytesConvertible + ) -> None: + """Delete account associated with an address.""" + self.assert_mutable() + if not isinstance(address, Address): + address = Address(address) + self._deleted_addresses.add(address) + self.__internal_delitem__(address) + + def __internal_delitem__( + self, + address: Address, + ) -> None: + """ + Delete account associated with an address. + + Called by the pre-alloc implementation to delete an account. + """ + self.root.pop(address, None) + + def deterministic_deploy_contract( + self, + *, + deploy_code: BytesConvertible, + salt: Hash | int = 0, + initcode: BytesConvertible | None = None, + storage: Storage | StorageRootType | None = None, + label: str | None = None, + ) -> Address: + """ + Deploy a contract to the allocation at a deterministic location + using a deterministic deployment proxy. + + The initcode is not executed during test filling; it is executed only + when the tests run on live networks. Therefore, if the initcode + performs modifications to the storage, these must be specified using + the `storage` parameter. + + Args: + deploy_code: Contract code to deploy. + salt: Salt to use for deterministic deployment. + initcode: Initcode to use for deterministic deployment. + If `None`, the initcode is derived from `deploy_code`. + storage: The expected storage state of the deployed contract after + initcode execution. + label: Label to use for the contract. + + """ + return self._deterministic_deploy_contract( + deploy_code=deploy_code, + salt=salt, + initcode=initcode, + storage=storage, + label=label, + ) + + def _deterministic_deploy_contract( + self, + *, + deploy_code: BytesConvertible, + salt: Hash | int, + initcode: BytesConvertible | None, + storage: Storage | StorageRootType | None, + label: str | None, + ) -> Address: + """ + Sub-class implementation of deterministic contract deployment. + """ + raise NotImplementedError( + "_deterministic_deploy_contract is not implemented in the base " + "class" + ) + + def deploy_contract( + self, + code: BytesConvertible, + *, + storage: Storage | StorageRootType | None = None, + balance: NumberConvertible = 0, + nonce: NumberConvertible = 1, + address: Address | None = None, + label: str | None = None, + stub: str | None = None, + ) -> Address: + """ + Deploy a contract to the allocation. + + Warning: `address` parameter is a temporary solution to allow tests to + hard-code the contract address. Do NOT use in new tests as it will be + removed in the future! + """ + if address is not None: + self.assert_mutable() + self._hardcoded_addresses_deployed_to.add(Address(address)) + + if Number(nonce) == 0: + self.assert_mutable() + + return self._deploy_contract( + code=code, + storage=storage, + balance=balance, + nonce=nonce, + address=address, + label=label, + stub=stub, + ) + + def _deploy_contract( + self, + code: BytesConvertible, + *, + storage: Storage | StorageRootType | None, + balance: NumberConvertible, + nonce: NumberConvertible, + address: Address | None, + label: str | None, + stub: str | None, + ) -> Address: + """ + Sub-class implementation of deploy_contract. + """ + raise NotImplementedError( + "_deploy_contract is not implemented in the base class" + ) + + def fund_eoa( + self, + amount: NumberConvertible | None = None, + label: str | None = None, + storage: Storage | None = None, + code: BytesConvertible | None = None, + delegation: Address | Literal["Self"] | None = None, + nonce: NumberConvertible | None = None, + ) -> EOA: + """ + Add a previously unused EOA to the pre-alloc with the balance specified + by `amount`. + + If amount is 0, nothing will be added to the pre-alloc but a new and + unique EOA will be returned. + """ + if code is not None: + self.assert_mutable() + + if nonce is not None: + self.assert_mutable() + + return self._fund_eoa( + amount=amount, + label=label, + storage=storage, + code=code, + delegation=delegation, + nonce=nonce, + ) + + def _fund_eoa( + self, + amount: NumberConvertible | None, + label: str | None, + storage: Storage | None, + code: BytesConvertible | None, + delegation: Address | Literal["Self"] | None, + nonce: NumberConvertible | None, + ) -> EOA: + """ + Sub-class implementation of fund_eoa. + """ + raise NotImplementedError( + "_fund_eoa is not implemented in the base class" + ) + + def fund_address( + self, + address: Address, + amount: NumberConvertible, + *, + minimum_balance: bool = False, + ) -> None: + """ + Fund an address with a given amount. + + Add a funded account to the pre-allocation. + The address must not already exist in the pre-allocation. To set the + balance of an account, use the `amount` parameter in `fund_eoa()` or + the `balance` parameter in `deploy_contract()` at creation time. + + Args: + address: Address to fund + amount: Amount to fund in Wei + minimum_balance: If set to True, account will be checked to have a + minimum balance of `amount` and only fund if the balance is + insufficient + + """ + if address in self: + raise Exception( + "Cannot fund an account already in state. " + "Use the appropriate `amount`, `balance` arguments " + "when creating the account." + ) + self._pre_funded_addresses.add(address) + return self._fund_address( + address=address, + amount=int(Number(amount)), + minimum_balance=minimum_balance, + ) + + def _fund_address( + self, + address: Address, + amount: int, + *, + minimum_balance: bool, + ) -> None: + """ + Sub-class implementation of fund_address. + """ + raise NotImplementedError( + "_fund_address is not implemented in the base class" + ) + + def empty_account(self) -> Address: + """ + Return a previously unused account guaranteed to be empty. + + This ensures the account has zero balance, zero nonce, no code, and no + storage. The account is not a precompile or a system contract. + """ + return self._empty_account() + + def _empty_account(self) -> Address: + """ + Sub-class implementation of empty_account. + """ + raise NotImplementedError( + "_empty_account is not implemented in the base class" + ) diff --git a/packages/testing/src/execution_testing/client_clis/clis/ethrex.py b/packages/testing/src/execution_testing/client_clis/clis/ethrex.py index 66ec2ece33..e488237ea3 100644 --- a/packages/testing/src/execution_testing/client_clis/clis/ethrex.py +++ b/packages/testing/src/execution_testing/client_clis/clis/ethrex.py @@ -38,6 +38,22 @@ class EthrexExceptionMapper(ExceptionMapper): BlockException.INVALID_BASEFEE_PER_GAS: ( "Base fee per gas is incorrect" ), + BlockException.INVALID_BLOCK_ACCESS_LIST: ( + "Block access list hash does not match the one in " + "the header after executing" + ), + BlockException.INVALID_BAL_HASH: ( + "Block access list hash does not match the one in " + "the header after executing" + ), + BlockException.INVALID_BAL_EXTRA_ACCOUNT: ( + "Block access list hash does not match the one in " + "the header after executing" + ), + BlockException.INVALID_BAL_MISSING_ACCOUNT: ( + "Block access list hash does not match the one in " + "the header after executing" + ), } mapping_regex = { TransactionException.PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS: ( @@ -116,6 +132,7 @@ class EthrexExceptionMapper(ExceptionMapper): TransactionException.GAS_ALLOWANCE_EXCEEDED: ( r"Gas allowance exceeded.*" ), + BlockException.GAS_USED_OVERFLOW: (r"Block gas used overflow.*"), TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED: ( r"Blob count exceeded.*" ), @@ -145,4 +162,16 @@ class EthrexExceptionMapper(ExceptionMapper): BlockException.RLP_BLOCK_LIMIT_EXCEEDED: ( r"Maximum block size exceeded.*" ), + BlockException.INVALID_BLOCK_ACCESS_LIST: ( + r"Block access list contains index \d+ " + r"exceeding max valid index \d+|" + r"Failed to RLP decode BAL" + ), + BlockException.INCORRECT_BLOCK_FORMAT: ( + r"Block access list hash does not match " + r"the one in the header after executing|" + r"Block access list contains index \d+ " + r"exceeding max valid index \d+|" + r"Failed to RLP decode BAL" + ), } diff --git a/packages/testing/src/execution_testing/forks/forks/forks.py b/packages/testing/src/execution_testing/forks/forks/forks.py index e638fb772f..471096a818 100644 --- a/packages/testing/src/execution_testing/forks/forks/forks.py +++ b/packages/testing/src/execution_testing/forks/forks/forks.py @@ -162,8 +162,6 @@ def gas_costs( G_MEMORY=3, G_TX_DATA_ZERO=4, G_TX_DATA_NON_ZERO=68, - G_TX_DATA_STANDARD_TOKEN_COST=0, - G_TX_DATA_FLOOR_TOKEN_COST=0, G_TRANSACTION=21_000, G_TRANSACTION_CREATE=32_000, G_LOG=375, @@ -173,8 +171,35 @@ def gas_costs( G_KECCAK_256_WORD=6, G_COPY=3, G_BLOCKHASH=20, + G_PRECOMPILE_ECRECOVER=3_000, + G_PRECOMPILE_SHA256_BASE=60, + G_PRECOMPILE_SHA256_WORD=12, + G_PRECOMPILE_RIPEMD160_BASE=600, + G_PRECOMPILE_RIPEMD160_WORD=120, + G_PRECOMPILE_IDENTITY_BASE=15, + G_PRECOMPILE_IDENTITY_WORD=3, + # Zero-initialized: introduced in later forks, set via + # replace() in the fork that activates them. + G_TX_DATA_STANDARD_TOKEN_COST=0, + G_TX_DATA_FLOOR_TOKEN_COST=0, G_AUTHORIZATION=0, R_AUTHORIZATION_EXISTING_AUTHORITY=0, + G_PRECOMPILE_ECADD=0, + G_PRECOMPILE_ECMUL=0, + G_PRECOMPILE_ECPAIRING_BASE=0, + G_PRECOMPILE_ECPAIRING_PER_POINT=0, + G_PRECOMPILE_BLAKE2F_BASE=0, + G_PRECOMPILE_BLAKE2F_PER_ROUND=0, + G_PRECOMPILE_POINT_EVALUATION=0, + G_PRECOMPILE_BLS_G1ADD=0, + G_PRECOMPILE_BLS_G1MUL=0, + G_PRECOMPILE_BLS_G1MAP=0, + G_PRECOMPILE_BLS_G2ADD=0, + G_PRECOMPILE_BLS_G2MUL=0, + G_PRECOMPILE_BLS_G2MAP=0, + G_PRECOMPILE_BLS_PAIRING_BASE=0, + G_PRECOMPILE_BLS_PAIRING_PER_PAIR=0, + G_PRECOMPILE_P256VERIFY=0, ) @classmethod @@ -1825,13 +1850,12 @@ def gas_costs( block_number=block_number, timestamp=timestamp ), G_TX_DATA_NON_ZERO=16, # https://eips.ethereum.org/EIPS/eip-2028 - # https://eips.ethereum.org/EIPS/eip-152 - G_BLAKE2_PER_ROUND=1, # https://eips.ethereum.org/EIPS/eip-1108 G_PRECOMPILE_ECADD=150, G_PRECOMPILE_ECMUL=6000, G_PRECOMPILE_ECPAIRING_BASE=45_000, G_PRECOMPILE_ECPAIRING_PER_POINT=34_000, + G_PRECOMPILE_BLAKE2F_PER_ROUND=1, ) @@ -2597,6 +2621,18 @@ def engine_new_payload_beacon_root( del block_number, timestamp return True + @classmethod + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: + """On Cancun, the point evaluation precompile gas cost is set.""" + return replace( + super(Cancun, cls).gas_costs( + block_number=block_number, timestamp=timestamp + ), + G_PRECOMPILE_POINT_EVALUATION=50_000, + ) + @classmethod def opcode_gas_map( cls, *, block_number: int = 0, timestamp: int = 0 @@ -2716,6 +2752,14 @@ def gas_costs( G_TX_DATA_FLOOR_TOKEN_COST=10, G_AUTHORIZATION=25_000, R_AUTHORIZATION_EXISTING_AUTHORITY=12_500, + G_PRECOMPILE_BLS_G1ADD=375, + G_PRECOMPILE_BLS_G1MUL=12_000, + G_PRECOMPILE_BLS_G1MAP=5_500, + G_PRECOMPILE_BLS_G2ADD=600, + G_PRECOMPILE_BLS_G2MUL=22_500, + G_PRECOMPILE_BLS_G2MAP=23_800, + G_PRECOMPILE_BLS_PAIRING_BASE=37_700, + G_PRECOMPILE_BLS_PAIRING_PER_PAIR=32_600, ) @classmethod @@ -3114,6 +3158,18 @@ def precompiles( block_number=block_number, timestamp=timestamp ) + @classmethod + def gas_costs( + cls, *, block_number: int = 0, timestamp: int = 0 + ) -> GasCosts: + """On Osaka, the P256VERIFY precompile gas cost is set.""" + return replace( + super(Osaka, cls).gas_costs( + block_number=block_number, timestamp=timestamp + ), + G_PRECOMPILE_P256VERIFY=6_900, + ) + @classmethod def excess_blob_gas_calculator( cls, *, block_number: int = 0, timestamp: int = 0 @@ -3258,7 +3314,7 @@ def gas_costs( super(MONAD_EIGHT, cls).gas_costs( block_number=block_number, timestamp=timestamp ), - G_BLAKE2_PER_ROUND=1 * 2, + G_PRECOMPILE_BLAKE2F_PER_ROUND=1 * 2, G_PRECOMPILE_ECADD=150 * 2, G_PRECOMPILE_ECMUL=6000 * 5, G_PRECOMPILE_ECPAIRING_BASE=45_000 * 5, diff --git a/packages/testing/src/execution_testing/forks/gas_costs.py b/packages/testing/src/execution_testing/forks/gas_costs.py index a441c0dd9f..0cf93b6aeb 100644 --- a/packages/testing/src/execution_testing/forks/gas_costs.py +++ b/packages/testing/src/execution_testing/forks/gas_costs.py @@ -62,11 +62,34 @@ class GasCosts: # Precompiled contract gas constants - G_BLAKE2_PER_ROUND: int = 0 - G_PRECOMPILE_ECADD: int = 0 - G_PRECOMPILE_ECMUL: int = 0 - G_PRECOMPILE_ECPAIRING_BASE: int = 0 - G_PRECOMPILE_ECPAIRING_PER_POINT: int = 0 + G_PRECOMPILE_ECRECOVER: int + G_PRECOMPILE_SHA256_BASE: int + G_PRECOMPILE_SHA256_WORD: int + G_PRECOMPILE_RIPEMD160_BASE: int + G_PRECOMPILE_RIPEMD160_WORD: int + G_PRECOMPILE_IDENTITY_BASE: int + G_PRECOMPILE_IDENTITY_WORD: int + + G_PRECOMPILE_ECADD: int + G_PRECOMPILE_ECMUL: int + G_PRECOMPILE_ECPAIRING_BASE: int + G_PRECOMPILE_ECPAIRING_PER_POINT: int + + G_PRECOMPILE_BLAKE2F_BASE: int + G_PRECOMPILE_BLAKE2F_PER_ROUND: int + + G_PRECOMPILE_POINT_EVALUATION: int + + G_PRECOMPILE_BLS_G1ADD: int + G_PRECOMPILE_BLS_G1MUL: int + G_PRECOMPILE_BLS_G1MAP: int + G_PRECOMPILE_BLS_G2ADD: int + G_PRECOMPILE_BLS_G2MUL: int + G_PRECOMPILE_BLS_G2MAP: int + G_PRECOMPILE_BLS_PAIRING_BASE: int + G_PRECOMPILE_BLS_PAIRING_PER_PAIR: int + + G_PRECOMPILE_P256VERIFY: int # Refund constants diff --git a/packages/testing/src/execution_testing/specs/base.py b/packages/testing/src/execution_testing/specs/base.py index fc8ce0d9f7..b7157a2f8a 100644 --- a/packages/testing/src/execution_testing/specs/base.py +++ b/packages/testing/src/execution_testing/specs/base.py @@ -2,7 +2,6 @@ Base test class and helper functions for Ethereum state and blockchain tests. """ -import hashlib from abc import abstractmethod from enum import StrEnum, unique from functools import reduce @@ -35,7 +34,6 @@ BaseFixture, FixtureFormat, LabeledFixtureFormat, - PreAllocGroupBuilders, ) from execution_testing.forks import Fork from execution_testing.forks.base_fork import BaseFork @@ -301,61 +299,5 @@ def get_genesis_environment(self) -> Environment: "access for use with pre-allocation groups." ) - def update_pre_alloc_groups( - self, pre_alloc_group_builders: PreAllocGroupBuilders, test_id: str - ) -> None: - """ - Create or update the pre-allocation group with the pre from the current - spec. - """ - if not hasattr(self, "pre"): - raise AttributeError( - f"{self.__class__.__name__} does not have a 'pre' field. " - "Pre-allocation groups are only supported for test types " - "that define pre-allocation." - ) - pre_alloc_hash = self.compute_pre_alloc_group_hash() - pre_alloc_group_builders.add_test_pre( - pre_alloc_hash=pre_alloc_hash, - test_id=str(test_id), - fork=self.fork, - environment=self.get_genesis_environment(), - pre=self.pre, - ) - - def compute_pre_alloc_group_hash(self) -> str: - """Hash (fork, env) in order to group tests by genesis config.""" - if not hasattr(self, "pre"): - raise AttributeError( - f"{self.__class__.__name__} does not have a 'pre' field. " - "Pre-allocation group usage is only supported for test " - "types that define pre-allocs." - ) - fork_digest = hashlib.sha256(self.fork.name().encode("utf-8")).digest() - fork_hash = int.from_bytes(fork_digest[:8], byteorder="big") - genesis_env = self.get_genesis_environment() - combined_hash = fork_hash ^ hash(genesis_env) - - # Check if test has pre_alloc_group marker - if self._request is not None and hasattr(self._request, "node"): - pre_alloc_group_marker = self._request.node.get_closest_marker( - "pre_alloc_group" - ) - if pre_alloc_group_marker: - # Get the group name/salt from marker args - if pre_alloc_group_marker.args: - group_salt = str(pre_alloc_group_marker.args[0]) - if group_salt == "separate": - # Use nodeid for unique group per test - group_salt = self._request.node.nodeid - # Add custom salt to hash - salt_hash = hashlib.sha256( - group_salt.encode("utf-8") - ).digest() - salt_int = int.from_bytes(salt_hash[:8], byteorder="big") - combined_hash = combined_hash ^ salt_int - - return f"0x{combined_hash:016x}" - TestSpec = Callable[[Fork], Generator[BaseTest, None, None]] diff --git a/packages/testing/src/execution_testing/specs/static_state/account.py b/packages/testing/src/execution_testing/specs/static_state/account.py index fe5c12aa11..84e2bbbcb5 100644 --- a/packages/testing/src/execution_testing/specs/static_state/account.py +++ b/packages/testing/src/execution_testing/specs/static_state/account.py @@ -1,16 +1,22 @@ """Account structure of ethereum/tests fillers.""" +import hashlib +import json from typing import Any, Dict, List, Mapping, Set, Tuple from pydantic import BaseModel, ConfigDict from execution_testing.base_types import ( - Bytes, + Account, EthereumTestRootModel, + Hash, HexNumber, - Storage, ) -from execution_testing.test_types import Alloc +from execution_testing.test_types import ( + Alloc, + contract_address_from_hash, + eoa_from_hash, +) from .common import ( AddressOrTagInFiller, @@ -87,6 +93,19 @@ def resolve(self, tags: TagDict) -> Dict[str, Any]: account_properties["storage"] = resolved_storage return account_properties + def hash(self) -> Hash: + """Return a hash of the account as it is in the filler.""" + dumped = self.model_dump(mode="json", exclude_none=True) + return Hash( + hashlib.sha256( + json.dumps( + dumped, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + ).digest() + ) + class PreInFiller(EthereumTestRootModel): """Class that represents a pre-state in filler.""" @@ -174,20 +193,21 @@ def setup(self, pre: Alloc, all_dependencies: Dict[str, Tag]) -> TagDict: # Step 4: Pre-deploy all contract tags and pre-fund EOAs to get # addresses + account_salts: Dict[Hash, int] = {} for tag_name in resolution_order: if tag_name in tag_to_address: tag = tag_to_address[tag_name] + account_hash = self.root[tag].hash() + salt = account_salts.get(account_hash, 0) + account_salts[account_hash] = salt + 1 if isinstance(tag, ContractTag): - # Deploy with placeholder to get address - deployed_address = pre.deploy_contract( - code=b"", # Temporary placeholder - label=tag_name, + # Get a placeholder address + resolved_accounts[tag_name] = contract_address_from_hash( + account_hash, salt ) - resolved_accounts[tag_name] = deployed_address elif isinstance(tag, SenderTag): - # Create EOA to get address - use amount=1 to ensure - # account is created - eoa = pre.fund_eoa(amount=1, label=tag_name) + # Create a placeholder EOA + eoa = eoa_from_hash(account_hash, salt) # Store the EOA object for SenderKeyTag resolution resolved_accounts[tag_name] = eoa @@ -203,62 +223,15 @@ def setup(self, pre: Alloc, all_dependencies: Dict[str, Tag]) -> TagDict: # All addresses are now available, so resolve properties account_properties = account.resolve(resolved_accounts) - if isinstance(tag, ContractTag): - # Update the already-deployed contract + if isinstance(tag, (ContractTag, SenderTag)): deployed_address = resolved_accounts[tag_name] - deployed_account = pre[deployed_address] - - if deployed_account is not None: - if "code" in account_properties: - deployed_account.code = Bytes( - account_properties["code"] - ) - if "balance" in account_properties: - deployed_account.balance = account_properties[ - "balance" - ] - if "nonce" in account_properties: - deployed_account.nonce = account_properties[ - "nonce" - ] - if "storage" in account_properties: - deployed_account.storage = Storage( - root=account_properties["storage"] - ) - - elif isinstance(tag, SenderTag): - eoa_account = pre[resolved_accounts[tag_name]] - - if eoa_account is not None: - if "balance" in account_properties: - eoa_account.balance = account_properties["balance"] - if "nonce" in account_properties: - eoa_account.nonce = account_properties["nonce"] - if "code" in account_properties: - eoa_account.code = Bytes( - account_properties["code"] - ) - if "storage" in account_properties: - eoa_account.storage = Storage( - root=account_properties["storage"] - ) + pre[deployed_address] = Account(**account_properties) # Step 6: Now process non-tagged accounts (including code compilation) - for address, account in non_tagged_to_process: - account_properties = account.resolve(resolved_accounts) - if "balance" in account_properties: - pre.fund_address(address, account_properties["balance"]) - - existing_account = pre[address] - if existing_account is not None: - if "code" in account_properties: - existing_account.code = Bytes(account_properties["code"]) - if "nonce" in account_properties: - existing_account.nonce = account_properties["nonce"] - if "storage" in account_properties: - existing_account.storage = Storage( - root=account_properties["storage"] - ) + for address, account_in_filler in non_tagged_to_process: + pre[address] = Account( + **account_in_filler.resolve(resolved_accounts) + ) # Step 7: Handle any extra dependencies not in pre for extra_dependency in all_dependencies: diff --git a/packages/testing/src/execution_testing/specs/static_state/state_static.py b/packages/testing/src/execution_testing/specs/static_state/state_static.py index 91f7217811..37619835a6 100644 --- a/packages/testing/src/execution_testing/specs/static_state/state_static.py +++ b/packages/testing/src/execution_testing/specs/static_state/state_static.py @@ -206,14 +206,8 @@ def test_state_vectors( if self.info and self.info.pytest_marks: for mark in self.info.pytest_marks: - if mark == "pre_alloc_group": - test_state_vectors = pytest.mark.pre_alloc_group( - "separate", - reason="Requires separate pre-alloc grouping", - )(test_state_vectors) - else: - apply_mark = getattr(pytest.mark, mark) - test_state_vectors = apply_mark(test_state_vectors) + apply_mark = getattr(pytest.mark, mark) + test_state_vectors = apply_mark(test_state_vectors) if has_tags: test_state_vectors = pytest.mark.tagged(test_state_vectors) @@ -223,13 +217,9 @@ def test_state_vectors( ) else: test_state_vectors = pytest.mark.untagged(test_state_vectors) - test_state_vectors = pytest.mark.pre_alloc_group( - "separate", reason="Uses hard-coded addresses" - )(test_state_vectors) - if not fully_tagged: - test_state_vectors = pytest.mark.pre_alloc_modify( - test_state_vectors - ) + + # All static tests are mutable since we do `pre[0x123...] = Account()` + test_state_vectors = pytest.mark.pre_alloc_mutable(test_state_vectors) return test_state_vectors diff --git a/packages/testing/src/execution_testing/test_types/__init__.py b/packages/testing/src/execution_testing/test_types/__init__.py index c87148f92a..979a76d49b 100644 --- a/packages/testing/src/execution_testing/test_types/__init__.py +++ b/packages/testing/src/execution_testing/test_types/__init__.py @@ -28,6 +28,8 @@ compute_create2_address, compute_create_address, compute_deterministic_create2_address, + contract_address_from_hash, + eoa_from_hash, ) from .phase_manager import TestPhase, TestPhaseManager from .receipt_types import TransactionLog, TransactionReceipt @@ -88,5 +90,7 @@ "compute_create_address", "compute_create2_address", "compute_deterministic_create2_address", + "contract_address_from_hash", + "eoa_from_hash", "keccak256", ) diff --git a/packages/testing/src/execution_testing/test_types/account_types.py b/packages/testing/src/execution_testing/test_types/account_types.py index 04106c4005..4cb216537a 100644 --- a/packages/testing/src/execution_testing/test_types/account_types.py +++ b/packages/testing/src/execution_testing/test_types/account_types.py @@ -442,6 +442,7 @@ def fund_eoa( amount: NumberConvertible | None = None, label: str | None = None, storage: Storage | None = None, + code: BytesConvertible | None = None, delegation: Address | Literal["Self"] | None = None, nonce: NumberConvertible | None = None, ) -> EOA: @@ -463,8 +464,10 @@ def fund_address( """ Fund an address with a given amount. - If the address is already present in the pre-alloc the amount will be - added to its existing balance. + Add a funded account to the pre-allocation. + The address must not already exist in the pre-allocation. To set the + balance of an account, use the `amount` parameter in `fund_eoa()` or + the `balance` parameter in `deploy_contract()` at creation time. Args: address: Address to fund diff --git a/packages/testing/src/execution_testing/test_types/helpers.py b/packages/testing/src/execution_testing/test_types/helpers.py index 7151141168..84931686ac 100644 --- a/packages/testing/src/execution_testing/test_types/helpers.py +++ b/packages/testing/src/execution_testing/test_types/helpers.py @@ -11,7 +11,7 @@ FixedSizeBytesConvertible, ) from execution_testing.forks import Fork -from execution_testing.vm import Op +from execution_testing.vm import Bytecode, Op from .account_types import EOA from .utils import int_to_bytes @@ -88,14 +88,18 @@ def compute_create_address( def compute_create2_address( address: FixedSizeBytesConvertible, salt: FixedSizeBytesConvertible, - initcode: BytesConvertible, + initcode: Bytecode | BytesConvertible, ) -> Address: """ Compute address of the resulting contract created using the `CREATE2` opcode. """ + if isinstance(initcode, Bytecode): + initcode_hash = initcode.keccak256() + else: + initcode_hash = Bytes(initcode).keccak256() hash_bytes = Bytes( - b"\xff" + Address(address) + Hash(salt) + Bytes(initcode).keccak256() + b"\xff" + Address(address) + Hash(salt) + initcode_hash ).keccak256() return Address(hash_bytes[-20:]) @@ -143,6 +147,28 @@ def add_kzg_version( return kzg_versioned_hashes +def contract_address_from_hash(account_hash: Hash, salt: int) -> Address: + """ + Calculate an address from a given (account) hash plus a salt. + + Useful to not duplicate accounts in the pre-allocation when grouping + many tests. + """ + return Address( + Bytes(account_hash + salt.to_bytes(64, "big")).sha256()[12:] + ) + + +def eoa_from_hash(account_hash: Hash, salt: int) -> EOA: + """ + Calculate an EOA from a given (account) hash plus a salt. + + Useful to not duplicate accounts in the pre-allocation when grouping + many tests. + """ + return EOA(key=Bytes(account_hash + salt.to_bytes(64, "big")).sha256()) + + class TestParameterGroup(BaseModel): """ Base class for grouping test parameters in a dataclass. Provides a generic diff --git a/packages/testing/src/execution_testing/tools/tools_code/generators.py b/packages/testing/src/execution_testing/tools/tools_code/generators.py index 924245842d..5c1be40ed7 100644 --- a/packages/testing/src/execution_testing/tools/tools_code/generators.py +++ b/packages/testing/src/execution_testing/tools/tools_code/generators.py @@ -6,12 +6,10 @@ from pydantic import Field from execution_testing.base_types import Address, Bytes -from execution_testing.forks import Fork -from execution_testing.test_types import EOA, Transaction, ceiling_division +from execution_testing.forks import Fork, Frontier +from execution_testing.test_types import EOA, Transaction from execution_testing.vm import Bytecode, ForkOpcodeInterface, Op -GAS_PER_DEPLOYED_CODE_BYTE = 0xC8 - class Initcode(Bytecode): """ @@ -47,13 +45,16 @@ def __new__( deploy_code: SupportsBytes | Bytes | None = None, initcode_length: int | None = None, initcode_prefix: Bytecode | None = None, - initcode_prefix_execution_gas: int = 0, padding_byte: int = 0x00, name: str = "", + fork: Fork = Frontier, ) -> Self: """ Generate legacy initcode that inits a contract with the specified code. The initcode can be padded to a specified length for testing purposes. + + Gas costs are calculated using the fork's gas costs and memory + expansion formula. Defaults to Frontier if no fork is provided. """ if deploy_code is None: deploy_code = Bytecode() @@ -62,19 +63,15 @@ def __new__( initcode = initcode_prefix code_length = len(bytes(deploy_code)) - execution_gas = initcode_prefix_execution_gas # PUSH2: length= initcode += Op.PUSH2(code_length) - execution_gas = 3 # PUSH1: offset=0 initcode += Op.PUSH1(0) - execution_gas += 3 # DUP2 initcode += Op.DUP2 - execution_gas += 3 # PUSH1: initcode_length=11 + len(initcode_prefix_bytes) (constant) no_prefix_length = 0x0B @@ -82,24 +79,17 @@ def __new__( "initcode prefix too long" ) initcode += Op.PUSH1(no_prefix_length + len(initcode_prefix)) - execution_gas += 3 # DUP3 initcode += Op.DUP3 - execution_gas += 3 # CODECOPY: destinationOffset=0, offset=0, length - initcode += Op.CODECOPY - execution_gas += ( - 3 - + (3 * ceiling_division(code_length, 32)) - + (3 * code_length) - + ((code_length * code_length) // 512) + initcode += Op.CODECOPY( + data_size=code_length, new_memory_size=code_length ) # RETURN: offset=0, length initcode += Op.RETURN - execution_gas += 0 initcode_plus_deploy_code = bytes(initcode) + bytes(deploy_code) padding_bytes = bytes() @@ -125,10 +115,10 @@ def __new__( ) instance._name_ = name instance.deploy_code = deploy_code - instance.execution_gas = execution_gas - instance.deployment_gas = GAS_PER_DEPLOYED_CODE_BYTE * len( - bytes(instance.deploy_code) - ) + instance.execution_gas = initcode.gas_cost(fork) + instance.deployment_gas = Op.RETURN( + code_deposit_size=len(bytes(instance.deploy_code)) + ).gas_cost(fork) return instance diff --git a/packages/testing/src/execution_testing/tools/utility/generators.py b/packages/testing/src/execution_testing/tools/utility/generators.py index 4a84c4ef98..c1e0eec500 100644 --- a/packages/testing/src/execution_testing/tools/utility/generators.py +++ b/packages/testing/src/execution_testing/tools/utility/generators.py @@ -179,7 +179,7 @@ def decorator(func: SystemContractDeployTestFunction) -> Callable: ], ids=lambda x: x.name.lower(), ) - @pytest.mark.execute(pytest.mark.skip(reason="modifies pre-alloc")) + @pytest.mark.pre_alloc_mutable @pytest.mark.valid_at_transition_to(fork.name()) def wrapper( blockchain_test: BlockchainTestFiller, @@ -248,9 +248,7 @@ def wrapper( nonce=0, balance=balance, ) - pre[deployer_address] = Account( - balance=deployer_required_balance, - ) + pre.fund_address(deployer_address, deployer_required_balance) expected_deploy_address_int = int.from_bytes( expected_deploy_address, "big" diff --git a/packages/testing/src/execution_testing/vm/bytecode.py b/packages/testing/src/execution_testing/vm/bytecode.py index 6c8dc984f9..4ef6d05e09 100644 --- a/packages/testing/src/execution_testing/vm/bytecode.py +++ b/packages/testing/src/execution_testing/vm/bytecode.py @@ -33,6 +33,15 @@ class Bytecode: _name_: str = "" _bytes_: bytes + _keccak_256_: Hash | None = None + _gas_cost_: int | None = None + _gas_cost_fork_: Type[ForkOpcodeInterface] | None = None + _gas_cost_block_number_: int | None = None + _gas_cost_timestamp_: int | None = None + _refund_: int | None = None + _refund_fork_: Type[ForkOpcodeInterface] | None = None + _refund_block_number_: int | None = None + _refund_timestamp_: int | None = None popped_stack_items: int pushed_stack_items: int @@ -261,7 +270,9 @@ def hex(self) -> str: def keccak256(self) -> Hash: """Return the keccak256 hash of the opcode byte representation.""" - return Bytes(self._bytes_).keccak256() + if self._keccak_256_ is None: + self._keccak_256_ = Bytes(self._bytes_).keccak256() + return self._keccak_256_ def gas_cost( self, @@ -271,13 +282,22 @@ def gas_cost( timestamp: int = 0, ) -> int: """Use a fork object to calculate the gas used by this bytecode.""" - opcode_gas_calculator = fork.opcode_gas_calculator( - block_number=block_number, timestamp=timestamp - ) - total_gas = 0 - for opcode in self.opcode_list: - total_gas += opcode_gas_calculator(opcode) - return total_gas + if ( + self._gas_cost_ is None + or self._gas_cost_fork_ != fork + or self._gas_cost_block_number_ != block_number + or self._gas_cost_timestamp_ != timestamp + ): + self._gas_cost_fork_ = fork + self._gas_cost_block_number_ = block_number + self._gas_cost_timestamp_ = timestamp + opcode_gas_calculator = fork.opcode_gas_calculator( + block_number=block_number, timestamp=timestamp + ) + self._gas_cost_ = 0 + for opcode in self.opcode_list: + self._gas_cost_ += opcode_gas_calculator(opcode) + return self._gas_cost_ def refund( self, @@ -287,13 +307,22 @@ def refund( timestamp: int = 0, ) -> int: """Use a fork object to calculate the gas refund from this bytecode.""" - opcode_refund_calculator = fork.opcode_refund_calculator( - block_number=block_number, timestamp=timestamp - ) - total_refund = 0 - for opcode in self.opcode_list: - total_refund += opcode_refund_calculator(opcode) - return total_refund + if ( + self._refund_ is None + or self._refund_fork_ != fork + or self._refund_block_number_ != block_number + or self._refund_timestamp_ != timestamp + ): + self._refund_fork_ = fork + self._refund_block_number_ = block_number + self._refund_timestamp_ = timestamp + opcode_refund_calculator = fork.opcode_refund_calculator( + block_number=block_number, timestamp=timestamp + ) + self._refund_ = 0 + for opcode in self.opcode_list: + self._refund_ += opcode_refund_calculator(opcode) + return self._refund_ @classmethod def __get_pydantic_core_schema__( diff --git a/pyproject.toml b/pyproject.toml index 3d31255db8..fee46a212a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -228,6 +228,7 @@ dev = [ { include-group = "doc" }, { include-group = "mkdocs" }, "ethereum-execution[optimized]", + "psutil>=7.2.2", ] [tool.setuptools.dynamic] diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py index 9f7400b433..1aff9d629f 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py @@ -1073,19 +1073,13 @@ def test_bal_noop_storage_write( ) -> None: """Test that BAL correctly handles no-op storage write.""" alice = pre.fund_eoa() - storage_contract = pre.deploy_contract( - code=Op.SSTORE(0x01, 0x42), storage={0x01: 0x42} + code = Op.SSTORE( + 0x01, 0x42, key_warm=False, original_value=0, new_value=0x42 ) + storage_contract = pre.deploy_contract(code=code, storage={0x01: 0x42}) intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - gas_limit = ( - intrinsic_gas_calculator() - # Sufficient gas for write - + fork.gas_costs().G_COLD_SLOAD - + fork.gas_costs().G_COLD_ACCOUNT_ACCESS - + fork.gas_costs().G_STORAGE_SET - + fork.gas_costs().G_BASE * 10 # Buffer for push - ) + gas_limit = intrinsic_gas_calculator() + code.gas_cost(fork) tx = Transaction( sender=alice, to=storage_contract, gas_limit=gas_limit, gas_price=0xA @@ -1387,10 +1381,6 @@ def test_bal_coinbase_zero_tip( ) -@pytest.mark.pre_alloc_group( - "precompile_funded", - reason="Expects clean precompile balances, isolate in EngineX", -) @pytest.mark.parametrize( "value", [ @@ -2198,10 +2188,6 @@ def test_bal_cross_tx_storage_revert_to_zero( ) -@pytest.mark.pre_alloc_group( - "ripemd160_state_leak", - reason="Pre-funds RIPEMD-160, must be isolated in EngineX format", -) def test_bal_cross_block_ripemd160_state_leak( pre: Alloc, blockchain_test: BlockchainTestFiller, @@ -2550,6 +2536,7 @@ def test_bal_all_transaction_types( ) +@pytest.mark.pre_alloc_mutable() def test_bal_lexicographic_address_ordering( pre: Alloc, blockchain_test: BlockchainTestFiller, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py index df35ba66ef..5f1781869d 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip4895.py @@ -623,10 +623,6 @@ def test_bal_zero_withdrawal( ) -@pytest.mark.pre_alloc_group( - "withdrawal_to_precompiles", - reason="Expects clean precompile balances, isolate in EngineX", -) @pytest.mark.parametrize_by_fork( "precompile", lambda fork: [ diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7251.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7251.py index a3ed19d957..c6be6cdecd 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7251.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_eip7251.py @@ -93,10 +93,6 @@ ), ], ) -@pytest.mark.pre_alloc_group( - "consolidation_requests", - reason="Tests standard consolidation request functionality", -) def test_bal_system_dequeue_consolidations_eip7251( blockchain_test: BlockchainTestFiller, pre: Alloc, diff --git a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py index c0e8886d35..aeaf5263b6 100644 --- a/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py +++ b/tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists_opcodes.py @@ -113,24 +113,25 @@ def test_bal_sstore_and_oog( 4. exact gas (success) -> storage write in BAL """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() # Create contract that attempts SSTORE to cold storage slot 0x01 - storage_contract_code = Bytecode(Op.SSTORE(0x01, 0x42)) + storage_contract_code = Op.SSTORE( + 0x01, 0x42, key_warm=False, original_value=0, new_value=0x42 + ) storage_contract = pre.deploy_contract(code=storage_contract_code) - intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_calculator() + intrinsic_gas_cost = fork.transaction_intrinsic_cost_calculator()() + + # Full cost: PUSHes + SSTORE (G_COLD_SLOAD + G_STORAGE_SET) + full_cost = storage_contract_code.gas_cost(fork) + + # Push cost for stipend boundary calculations + push_code = Op.PUSH1(0x42) + Op.PUSH1(0x01) + push_cost = push_code.gas_cost(fork) - # Costs: - # - PUSH1 (value and slot) = G_VERY_LOW * 2 - # - SSTORE cold (to zero slot) = G_STORAGE_SET + G_COLD_SLOAD - sload_cost = gas_costs.G_COLD_SLOAD - sstore_cost = gas_costs.G_STORAGE_SET - sstore_cold_cost = sstore_cost + sload_cost - push_cost = gas_costs.G_VERY_LOW * 2 - stipend = gas_costs.G_CALL_STIPEND + # G_CALL_STIPEND is a threshold check, not a gas cost — keep from gas_costs + stipend = fork.gas_costs().G_CALL_STIPEND if out_of_gas_at == OutOfGasAt.EIP_2200_STIPEND: # 2300 after PUSHes (fails stipend check: 2300 <= 2300) @@ -140,10 +141,10 @@ def test_bal_sstore_and_oog( tx_gas_limit = intrinsic_gas_cost + push_cost + stipend + 1 elif out_of_gas_at == OutOfGasAt.EXACT_GAS_MINUS_1: # fail at charge_gas() at exact gas - 1 (boundary condition) - tx_gas_limit = intrinsic_gas_cost + push_cost + sstore_cold_cost - 1 + tx_gas_limit = intrinsic_gas_cost + full_cost - 1 else: # exact gas for successful SSTORE - tx_gas_limit = intrinsic_gas_cost + push_cost + sstore_cold_cost + tx_gas_limit = intrinsic_gas_cost + full_cost tx = Transaction( sender=alice, @@ -209,26 +210,19 @@ def test_bal_sload_and_oog( Ensure BAL handles SLOAD and OOG during SLOAD appropriately. """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() # Create contract that attempts SLOAD from cold storage slot 0x01 - storage_contract_code = Bytecode( + storage_contract_code = ( Op.PUSH1(0x01) # Storage slot (cold) - + Op.SLOAD # Load value from slot - this will OOG + + Op.SLOAD(key_warm=False) # Load value from slot - this will OOG + Op.STOP ) storage_contract = pre.deploy_contract(code=storage_contract_code) - intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_calculator() + intrinsic_gas_cost = fork.transaction_intrinsic_cost_calculator()() - # Costs: - # - PUSH1 (slot) = G_VERY_LOW - # - SLOAD cold = G_COLD_SLOAD - push_cost = gas_costs.G_VERY_LOW - sload_cold_cost = gas_costs.G_COLD_SLOAD - tx_gas_limit = intrinsic_gas_cost + push_cost + sload_cold_cost + tx_gas_limit = intrinsic_gas_cost + storage_contract_code.gas_cost(fork) if fails_at_sload: # subtract 1 gas to ensure OOG at SLOAD @@ -275,26 +269,19 @@ def test_bal_balance_and_oog( """Ensure BAL handles BALANCE and OOG during BALANCE appropriately.""" alice = pre.fund_eoa() bob = pre.fund_eoa() - gas_costs = fork.gas_costs() # Create contract that attempts to check Bob's balance - balance_checker_code = Bytecode( + balance_checker_code = ( Op.PUSH20(bob) # Bob's address - + Op.BALANCE # Check balance (cold access) + + Op.BALANCE(address_warm=False) # Check balance (cold access) + Op.STOP ) balance_checker = pre.deploy_contract(code=balance_checker_code) - intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_calculator() + intrinsic_gas_cost = fork.transaction_intrinsic_cost_calculator()() - # Costs: - # - PUSH20 = G_VERY_LOW - # - BALANCE cold = G_COLD_ACCOUNT_ACCESS - push_cost = gas_costs.G_VERY_LOW - balance_cold_cost = gas_costs.G_COLD_ACCOUNT_ACCESS - tx_gas_limit = intrinsic_gas_cost + push_cost + balance_cold_cost + tx_gas_limit = intrinsic_gas_cost + balance_checker_code.gas_cost(fork) if fails_at_balance: # subtract 1 gas to ensure OOG at BALANCE @@ -347,29 +334,22 @@ def test_bal_extcodesize_and_oog( Ensure BAL handles EXTCODESIZE and OOG during EXTCODESIZE appropriately. """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() # Create target contract with some code - target_contract = pre.deploy_contract(code=Bytecode(Op.STOP)) + target_contract = pre.deploy_contract(code=Op.STOP) # Create contract that checks target's code size - codesize_checker_code = Bytecode( + codesize_checker_code = ( Op.PUSH20(target_contract) # Target contract address - + Op.EXTCODESIZE # Check code size (cold access) + + Op.EXTCODESIZE(address_warm=False) # Check code size (cold access) + Op.STOP ) codesize_checker = pre.deploy_contract(code=codesize_checker_code) - intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_calculator() + intrinsic_gas_cost = fork.transaction_intrinsic_cost_calculator()() - # Costs: - # - PUSH20 = G_VERY_LOW - # - EXTCODESIZE cold = G_COLD_ACCOUNT_ACCESS - push_cost = gas_costs.G_VERY_LOW - extcodesize_cold_cost = gas_costs.G_COLD_ACCOUNT_ACCESS - tx_gas_limit = intrinsic_gas_cost + push_cost + extcodesize_cold_cost + tx_gas_limit = intrinsic_gas_cost + codesize_checker_code.gas_cost(fork) if fails_at_extcodesize: # subtract 1 gas to ensure OOG at EXTCODESIZE @@ -438,7 +418,6 @@ def test_bal_call_no_delegation_and_oog_before_target_access( When target_is_warm=True, we use EIP-2930 tx access list to warm the target. Access list warming does NOT add to BAL - only EVM access does. """ - gas_costs = fork.gas_costs() alice = pre.fund_eoa() target = ( @@ -449,8 +428,17 @@ def test_bal_call_no_delegation_and_oog_before_target_access( ret_size = 32 if memory_expansion else 0 + # Full gas metadata: includes create_cost when applicable call_code = Op.CALL( - gas=0, address=target, value=value, ret_size=ret_size, ret_offset=0 + gas=0, + address=target, + value=value, + ret_size=ret_size, + ret_offset=0, + address_warm=target_is_warm, + value_transfer=value > 0, + account_new=value > 0 and target_is_empty, + new_memory_size=ret_size, ) caller = pre.deploy_contract(code=call_code, balance=value) @@ -464,30 +452,22 @@ def test_bal_call_no_delegation_and_oog_before_target_access( access_list=access_list ) - bytecode_cost = gas_costs.G_VERY_LOW * 7 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - transfer_cost = gas_costs.G_CALL_VALUE if value > 0 else 0 - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - - # Create cost: only if value > 0 AND target is empty - create_cost = ( - gas_costs.G_NEW_ACCOUNT if (value > 0 and target_is_empty) else 0 - ) - - # static gas (before state access): access + transfer + memory - static_gas_cost = access_cost + transfer_cost + memory_cost - # second check includes create_cost - second_check_cost = static_gas_cost + create_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + # Static gas (before state access): no create_cost + call_static = Op.CALL( + gas=0, + address=target, + value=value, + ret_size=ret_size, + ret_offset=0, + address_warm=target_is_warm, + value_transfer=value > 0, + account_new=False, + new_memory_size=ret_size, + ) + gas_limit = intrinsic_cost + call_static.gas_cost(fork) - 1 else: # SUCCESS - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost + gas_limit = intrinsic_cost + call_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -583,7 +563,6 @@ def test_bal_call_no_delegation_oog_after_target_access( to empty accounts, creating the gap tested here. """ - gas_costs = fork.gas_costs() alice = pre.fund_eoa() # empty target required for create_cost gap @@ -594,9 +573,18 @@ def test_bal_call_no_delegation_oog_after_target_access( # memory expansion / no expansion ret_size = 32 if memory_expansion else 0 - # caller contract - no warmup code, we use tx access list instead + # Static gas (before state access): no create_cost + # Pass static check, fail at second check due to create cost call_code = Op.CALL( - gas=0, address=target, value=value, ret_size=ret_size, ret_offset=0 + gas=0, + address=target, + value=value, + ret_size=ret_size, + ret_offset=0, + address_warm=target_is_warm, + value_transfer=True, + account_new=False, + new_memory_size=ret_size, ) caller = pre.deploy_contract(code=call_code, balance=value) @@ -611,24 +599,7 @@ def test_bal_call_no_delegation_oog_after_target_access( access_list=access_list ) - # Bytecode cost: 7 pushes for Op.CALL (no warmup code) - bytecode_cost = gas_costs.G_VERY_LOW * 7 - - # Access cost for CALL - warm if in tx access list - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - transfer_cost = gas_costs.G_CALL_VALUE # value > 0, so always charged - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - - # static gas cost (before state access): access + transfer + memory - static_gas_cost = access_cost + transfer_cost + memory_cost - - # Pass static check, fail at second check due to create cost - # (create_cost = G_NEW_ACCOUNT = 25000 for empty target + value > 0) - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + call_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -697,7 +668,6 @@ def test_bal_call_7702_delegation_and_oog( When target_is_warm or delegation_is_warm, we use EIP-2930 tx access list. Access list warming does NOT add targets to BAL - only EVM access does. """ - gas_costs = fork.gas_costs() alice = pre.fund_eoa() delegation_target = pre.deploy_contract(code=Op.STOP) @@ -706,12 +676,19 @@ def test_bal_call_7702_delegation_and_oog( # memory expansion / no expansion ret_size = 32 if memory_expansion else 0 + # Full gas metadata: includes delegation cost call_code = Op.CALL( gas=0, address=target, value=value, ret_size=ret_size, ret_offset=0, + address_warm=target_is_warm, + value_transfer=value > 0, + account_new=False, + new_memory_size=ret_size, + delegated_address=True, + delegated_address_warm=delegation_is_warm, ) caller = pre.deploy_contract(code=call_code, balance=value) @@ -728,36 +705,29 @@ def test_bal_call_7702_delegation_and_oog( access_list=access_list ) - bytecode_cost = gas_costs.G_VERY_LOW * 7 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - transfer_cost = gas_costs.G_CALL_VALUE if value > 0 else 0 - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - delegation_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if delegation_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS + # Static gas (before state access): no delegation + call_static = Op.CALL( + gas=0, + address=target, + value=value, + ret_size=ret_size, + ret_offset=0, + address_warm=target_is_warm, + value_transfer=value > 0, + account_new=False, + new_memory_size=ret_size, ) - static_gas_cost = access_cost + transfer_cost + memory_cost - - # The EVM's second check cost is static_gas + delegation_cost. - second_check_cost = static_gas_cost + delegation_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + gas_limit = intrinsic_cost + call_static.gas_cost(fork) - 1 elif oog_boundary == OutOfGasBoundary.OOG_AFTER_TARGET_ACCESS: # Enough for static_gas only - not enough for delegation_cost - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + call_static.gas_cost(fork) elif oog_boundary == OutOfGasBoundary.OOG_SUCCESS_MINUS_1: - # One less than second_check_cost - not enough for full call - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost - 1 + # One less than full cost - not enough for full call + gas_limit = intrinsic_cost + call_code.gas_cost(fork) - 1 else: - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost + gas_limit = intrinsic_cost + call_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -857,7 +827,6 @@ def test_bal_delegatecall_no_delegation_and_oog_before_target_access( target. Access list warming does NOT add to BAL - only EVM access does. """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() target = pre.deploy_contract(code=Op.STOP) @@ -869,6 +838,8 @@ def test_bal_delegatecall_no_delegation_and_oog_before_target_access( gas=0, ret_size=ret_size, ret_offset=ret_offset, + address_warm=target_is_warm, + new_memory_size=ret_size, ) caller = pre.deploy_contract(code=delegatecall_code) @@ -883,24 +854,10 @@ def test_bal_delegatecall_no_delegation_and_oog_before_target_access( access_list=access_list ) - # 6 pushes: retSize, retOffset, argsSize, argsOffset, address, gas - bytecode_cost = gas_costs.G_VERY_LOW * 6 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - - # static gas (before state access) == second check (no delegation cost) - static_gas_cost = access_cost + memory_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + gas_limit = intrinsic_cost + delegatecall_code.gas_cost(fork) - 1 else: # SUCCESS - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + delegatecall_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -973,7 +930,6 @@ def test_bal_delegatecall_7702_delegation_and_oog( behaviors. """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() delegation_target = pre.deploy_contract(code=Op.STOP) target = pre.fund_eoa(amount=0, delegation=delegation_target) @@ -982,11 +938,16 @@ def test_bal_delegatecall_7702_delegation_and_oog( ret_size = 32 if memory_expansion else 0 ret_offset = 0 + # Full gas metadata: includes delegation cost delegatecall_code = Op.DELEGATECALL( gas=0, address=target, ret_size=ret_size, ret_offset=ret_offset, + address_warm=target_is_warm, + new_memory_size=ret_size, + delegated_address=True, + delegated_address_warm=delegation_is_warm, ) caller = pre.deploy_contract(code=delegatecall_code) @@ -1004,35 +965,26 @@ def test_bal_delegatecall_7702_delegation_and_oog( access_list=access_list ) - bytecode_cost = gas_costs.G_VERY_LOW * 6 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - delegation_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if delegation_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS + # Static gas (before state access): no delegation + delegatecall_static = Op.DELEGATECALL( + gas=0, + address=target, + ret_size=ret_size, + ret_offset=ret_offset, + address_warm=target_is_warm, + new_memory_size=ret_size, ) - static_gas_cost = access_cost + memory_cost - - # The EVM's second check cost is static_gas + delegation_cost. - second_check_cost = static_gas_cost + delegation_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + gas_limit = intrinsic_cost + delegatecall_static.gas_cost(fork) - 1 elif oog_boundary == OutOfGasBoundary.OOG_AFTER_TARGET_ACCESS: # Enough for static_gas only - not enough for delegation_cost - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + delegatecall_static.gas_cost(fork) elif oog_boundary == OutOfGasBoundary.OOG_SUCCESS_MINUS_1: - # One less than second_check_cost - not enough for full call - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost - 1 + # One less than full cost - not enough for full call + gas_limit = intrinsic_cost + delegatecall_code.gas_cost(fork) - 1 else: - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost + gas_limit = intrinsic_cost + delegatecall_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -1111,7 +1063,6 @@ def test_bal_callcode_no_delegation_and_oog_before_target_access( target. Access list warming does NOT add to BAL - only EVM access does. CALLCODE has no balance transfer to target (runs in caller's context). """ - gas_costs = fork.gas_costs() alice = pre.fund_eoa() target = pre.deploy_contract(code=Op.STOP) @@ -1119,7 +1070,15 @@ def test_bal_callcode_no_delegation_and_oog_before_target_access( ret_size = 32 if memory_expansion else 0 callcode_code = Op.CALLCODE( - gas=0, address=target, value=value, ret_size=ret_size, ret_offset=0 + gas=0, + address=target, + value=value, + ret_size=ret_size, + ret_offset=0, + address_warm=target_is_warm, + value_transfer=value > 0, + account_new=False, + new_memory_size=ret_size, ) caller = pre.deploy_contract(code=callcode_code, balance=value) @@ -1133,23 +1092,10 @@ def test_bal_callcode_no_delegation_and_oog_before_target_access( access_list=access_list ) - bytecode_cost = gas_costs.G_VERY_LOW * 7 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - transfer_cost = gas_costs.G_CALL_VALUE if value > 0 else 0 - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - - # static gas: access + transfer + memory (== second check, no delegation) - static_gas_cost = access_cost + transfer_cost + memory_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + gas_limit = intrinsic_cost + callcode_code.gas_cost(fork) - 1 else: # SUCCESS - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + callcode_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -1230,7 +1176,6 @@ def test_bal_callcode_7702_delegation_and_oog( second check (delegation_cost) - all 3 scenarios produce distinct behaviors. """ - gas_costs = fork.gas_costs() alice = pre.fund_eoa() delegation_target = pre.deploy_contract(code=Op.STOP) @@ -1239,12 +1184,19 @@ def test_bal_callcode_7702_delegation_and_oog( # memory expansion / no expansion ret_size = 32 if memory_expansion else 0 + # Full gas metadata: includes delegation cost callcode_code = Op.CALLCODE( gas=0, address=target, value=value, ret_size=ret_size, ret_offset=0, + address_warm=target_is_warm, + value_transfer=value > 0, + account_new=False, + new_memory_size=ret_size, + delegated_address=True, + delegated_address_warm=delegation_is_warm, ) caller = pre.deploy_contract(code=callcode_code, balance=value) @@ -1261,36 +1213,29 @@ def test_bal_callcode_7702_delegation_and_oog( access_list=access_list ) - bytecode_cost = gas_costs.G_VERY_LOW * 7 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - transfer_cost = gas_costs.G_CALL_VALUE if value > 0 else 0 - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - delegation_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if delegation_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS + # Static gas (before state access): no delegation + callcode_static = Op.CALLCODE( + gas=0, + address=target, + value=value, + ret_size=ret_size, + ret_offset=0, + address_warm=target_is_warm, + value_transfer=value > 0, + account_new=False, + new_memory_size=ret_size, ) - static_gas_cost = access_cost + transfer_cost + memory_cost - - # The EVM's second check cost is static_gas + delegation_cost. - second_check_cost = static_gas_cost + delegation_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + gas_limit = intrinsic_cost + callcode_static.gas_cost(fork) - 1 elif oog_boundary == OutOfGasBoundary.OOG_AFTER_TARGET_ACCESS: # Enough for static_gas only - not enough for delegation_cost - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + callcode_static.gas_cost(fork) elif oog_boundary == OutOfGasBoundary.OOG_SUCCESS_MINUS_1: - # One less than second_check_cost - not enough for full call - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost - 1 + # One less than full cost - not enough for full call + gas_limit = intrinsic_cost + callcode_code.gas_cost(fork) - 1 else: - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost + gas_limit = intrinsic_cost + callcode_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -1367,7 +1312,6 @@ def test_bal_staticcall_no_delegation_and_oog_before_target_access( target. Access list warming does NOT add to BAL - only EVM access does. """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() target = pre.deploy_contract(code=Op.STOP) @@ -1379,6 +1323,8 @@ def test_bal_staticcall_no_delegation_and_oog_before_target_access( gas=0, ret_size=ret_size, ret_offset=ret_offset, + address_warm=target_is_warm, + new_memory_size=ret_size, ) caller = pre.deploy_contract(code=staticcall_code) @@ -1393,24 +1339,10 @@ def test_bal_staticcall_no_delegation_and_oog_before_target_access( access_list=access_list ) - # 6 pushes: retSize, retOffset, argsSize, argsOffset, address, gas - bytecode_cost = gas_costs.G_VERY_LOW * 6 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - - # static gas (before state access) == second check (no delegation cost) - static_gas_cost = access_cost + memory_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + gas_limit = intrinsic_cost + staticcall_code.gas_cost(fork) - 1 else: # SUCCESS - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + staticcall_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -1483,7 +1415,6 @@ def test_bal_staticcall_7702_delegation_and_oog( behaviors. """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() delegation_target = pre.deploy_contract(code=Op.STOP) target = pre.fund_eoa(amount=0, delegation=delegation_target) @@ -1492,11 +1423,16 @@ def test_bal_staticcall_7702_delegation_and_oog( ret_size = 32 if memory_expansion else 0 ret_offset = 0 + # Full gas metadata: includes delegation cost staticcall_code = Op.STATICCALL( gas=0, address=target, ret_size=ret_size, ret_offset=ret_offset, + address_warm=target_is_warm, + new_memory_size=ret_size, + delegated_address=True, + delegated_address_warm=delegation_is_warm, ) caller = pre.deploy_contract(code=staticcall_code) @@ -1514,35 +1450,26 @@ def test_bal_staticcall_7702_delegation_and_oog( access_list=access_list ) - bytecode_cost = gas_costs.G_VERY_LOW * 6 - - access_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if target_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) - memory_cost = fork.memory_expansion_gas_calculator()(new_bytes=ret_size) - delegation_cost = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if delegation_is_warm - else gas_costs.G_COLD_ACCOUNT_ACCESS + # Static gas (before state access): no delegation + staticcall_static = Op.STATICCALL( + gas=0, + address=target, + ret_size=ret_size, + ret_offset=ret_offset, + address_warm=target_is_warm, + new_memory_size=ret_size, ) - static_gas_cost = access_cost + memory_cost - - # The EVM's second check cost is static_gas + delegation_cost - second_check_cost = static_gas_cost + delegation_cost - if oog_boundary == OutOfGasBoundary.OOG_BEFORE_TARGET_ACCESS: - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost - 1 + gas_limit = intrinsic_cost + staticcall_static.gas_cost(fork) - 1 elif oog_boundary == OutOfGasBoundary.OOG_AFTER_TARGET_ACCESS: # Enough for static_gas only - not enough for delegation_cost - gas_limit = intrinsic_cost + bytecode_cost + static_gas_cost + gas_limit = intrinsic_cost + staticcall_static.gas_cost(fork) elif oog_boundary == OutOfGasBoundary.OOG_SUCCESS_MINUS_1: - # One less than second_check_cost - not enough for full call - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost - 1 + # One less than full cost - not enough for full call + gas_limit = intrinsic_cost + staticcall_code.gas_cost(fork) - 1 else: - gas_limit = intrinsic_cost + bytecode_cost + second_check_cost + gas_limit = intrinsic_cost + staticcall_code.gas_cost(fork) tx = Transaction( sender=alice, @@ -1635,65 +1562,60 @@ def test_bal_extcodecopy_and_oog( checked BEFORE recording account access. """ alice = pre.fund_eoa() - gas_costs = fork.gas_costs() # Create target contract with some code - target_contract = pre.deploy_contract( - code=Bytecode(Op.PUSH1(0x42) + Op.STOP) - ) + target_contract = pre.deploy_contract(code=Op.PUSH1(0x42) + Op.STOP) - # Build EXTCODECOPY contract with appropriate PUSH sizes - if memory_offset <= 0xFF: - dest_push = Op.PUSH1(memory_offset) - elif memory_offset <= 0xFFFF: - dest_push = Op.PUSH2(memory_offset) - else: - dest_push = Op.PUSH3(memory_offset) - - extcodecopy_contract_code = Bytecode( - Op.PUSH1(copy_size) - + Op.PUSH1(0) # codeOffset - + dest_push # destOffset - + Op.PUSH20(target_contract) - + Op.EXTCODECOPY - + Op.STOP + # Full EXTCODECOPY: access + copy + memory expansion + extcodecopy_code = Op.EXTCODECOPY( + address=target_contract, + dest_offset=memory_offset, + offset=0, + size=copy_size, + address_warm=False, + data_size=copy_size, + new_memory_size=memory_offset + copy_size, ) - extcodecopy_contract = pre.deploy_contract(code=extcodecopy_contract_code) - - intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() - intrinsic_gas_cost = intrinsic_gas_calculator() + extcodecopy_contract = pre.deploy_contract(code=extcodecopy_code + Op.STOP) - # Calculate costs - push_cost = gas_costs.G_VERY_LOW * 4 - cold_access_cost = gas_costs.G_COLD_ACCOUNT_ACCESS - copy_cost = gas_costs.G_COPY * ((copy_size + 31) // 32) + intrinsic_gas_cost = fork.transaction_intrinsic_cost_calculator()() if oog_scenario == "success": # Provide enough gas for everything including memory expansion - memory_cost = fork.memory_expansion_gas_calculator()( - new_bytes=memory_offset + copy_size - ) - execution_cost = push_cost + cold_access_cost + copy_cost + memory_cost - tx_gas_limit = intrinsic_gas_cost + execution_cost + tx_gas_limit = intrinsic_gas_cost + extcodecopy_code.gas_cost(fork) target_in_bal = True elif oog_scenario == "oog_at_cold_access": - # Provide gas for pushes but 1 less than cold access cost - execution_cost = push_cost + cold_access_cost - tx_gas_limit = intrinsic_gas_cost + execution_cost - 1 + # Provide gas for pushes but 1 less than cold access + extcodecopy_access_only = Op.EXTCODECOPY( + address=target_contract, + dest_offset=memory_offset, + offset=0, + size=copy_size, + address_warm=False, + data_size=0, + new_memory_size=0, + ) + tx_gas_limit = ( + intrinsic_gas_cost + extcodecopy_access_only.gas_cost(fork) - 1 + ) target_in_bal = False elif oog_scenario == "oog_at_memory_large_offset": # Provide gas for push + cold access + copy, but NOT memory expansion - execution_cost = push_cost + cold_access_cost + copy_cost - tx_gas_limit = intrinsic_gas_cost + execution_cost + extcodecopy_no_mem = Op.EXTCODECOPY( + address=target_contract, + dest_offset=memory_offset, + offset=0, + size=copy_size, + address_warm=False, + data_size=copy_size, + new_memory_size=0, + ) + tx_gas_limit = intrinsic_gas_cost + extcodecopy_no_mem.gas_cost(fork) target_in_bal = False elif oog_scenario == "oog_at_memory_boundary": - # Calculate memory cost and provide exactly 1 less than needed - memory_cost = fork.memory_expansion_gas_calculator()( - new_bytes=memory_offset + copy_size - ) - execution_cost = push_cost + cold_access_cost + copy_cost + memory_cost - tx_gas_limit = intrinsic_gas_cost + execution_cost - 1 + # Calculate full cost and provide exactly 1 less than needed + tx_gas_limit = intrinsic_gas_cost + extcodecopy_code.gas_cost(fork) - 1 target_in_bal = False else: raise ValueError(f"Invariant: unknown oog_scenario {oog_scenario}") @@ -2434,6 +2356,7 @@ def test_bal_create_selfdestruct_to_self_with_call( ) +@pytest.mark.pre_alloc_mutable() def test_bal_create2_collision( pre: Alloc, blockchain_test: BlockchainTestFiller, diff --git a/tests/benchmark/compute/helpers.py b/tests/benchmark/compute/helpers.py index c3281ed43f..07d93338fc 100644 --- a/tests/benchmark/compute/helpers.py +++ b/tests/benchmark/compute/helpers.py @@ -2,7 +2,7 @@ import math from enum import Enum, auto -from typing import Generator, Self, Sequence, cast +from typing import Dict, Generator, Self, Sequence, cast from execution_testing import ( EOA, @@ -349,6 +349,8 @@ class CustomSizedContractFactory(IteratingBytecode): _cached_address: Address """Cached address to avoid expensive recomputation.""" + _cached_created_contracts: Dict[int, Address] + """Cached created contract addresses to avoid expensive recomputation.""" contract_size: int """The size of the contracts to deploy.""" @@ -418,6 +420,7 @@ def __new__( initcode=Initcode(deploy_code=instance), fork=fork, ) + instance._cached_created_contracts = {} instance.contract_size = initcode.contract_size deployed_address = pre.deterministic_deploy_contract( deploy_code=instance @@ -494,8 +497,10 @@ def address(self) -> Address: def created_contract_address(self, *, salt: int) -> Address: """Get the deterministic address of the created contract.""" - return compute_create2_address( - address=self.address(), - salt=salt, - initcode=self.initcode, - ) + if salt not in self._cached_created_contracts: + self._cached_created_contracts[salt] = compute_create2_address( + address=self.address(), + salt=salt, + initcode=self.initcode, + ) + return self._cached_created_contracts[salt] diff --git a/tests/benchmark/compute/instruction/test_account_query.py b/tests/benchmark/compute/instruction/test_account_query.py index 590e70b9ea..082e37135a 100644 --- a/tests/benchmark/compute/instruction/test_account_query.py +++ b/tests/benchmark/compute/instruction/test_account_query.py @@ -13,7 +13,7 @@ """ import math -from typing import Any +from typing import Any, Dict import pytest from execution_testing import ( @@ -539,17 +539,25 @@ def calldata(iteration_count: int, start_iteration: int) -> bytes: # Access list generator for warm access tests. # When access_warm=True, include all contract addresses that will be # accessed in each transaction to warm them up via access list. + # Note: This access list generation is very expensive due to the binary + # search, which builds different access lists using the same elements + # over and over. Caching the elements helps a bit. + access_list_cache: Dict[int, AccessList] = {} + def access_list_generator( iteration_count: int, start_iteration: int ) -> list[AccessList] | None: if not access_warm: return None return [ - AccessList( - address=custom_sized_contract_factory.created_contract_address( - salt=i + access_list_cache.setdefault( + i, + AccessList( + address=custom_sized_contract_factory.created_contract_address( + salt=i + ), + storage_keys=[], ), - storage_keys=[], ) for i in range(start_iteration, start_iteration + iteration_count) ] diff --git a/tests/benchmark/compute/instruction/test_system.py b/tests/benchmark/compute/instruction/test_system.py index 87fbdcf67f..ad1af5a4d4 100644 --- a/tests/benchmark/compute/instruction/test_system.py +++ b/tests/benchmark/compute/instruction/test_system.py @@ -171,6 +171,7 @@ def test_create( Op.CREATE2, ], ) +@pytest.mark.pre_alloc_mutable def test_creates_collisions( benchmark_test: BenchmarkTestFiller, pre: Alloc, diff --git a/tests/benchmark/compute/instruction/test_tx_context.py b/tests/benchmark/compute/instruction/test_tx_context.py index 3c4975fc93..51148b1585 100644 --- a/tests/benchmark/compute/instruction/test_tx_context.py +++ b/tests/benchmark/compute/instruction/test_tx_context.py @@ -42,6 +42,9 @@ def test_call_frame_context_ops( @pytest.mark.repricing +@pytest.mark.execute( + pytest.mark.skip(reason="type 3 tx not supported in execute") +) @pytest.mark.parametrize( "blob_present", [ diff --git a/tests/benchmark/stateful/bloatnet/stubs.json b/tests/benchmark/stateful/bloatnet/stubs_bloatnet.json similarity index 100% rename from tests/benchmark/stateful/bloatnet/stubs.json rename to tests/benchmark/stateful/bloatnet/stubs_bloatnet.json diff --git a/tests/benchmark/stateful/bloatnet/stubs_mainnet.json b/tests/benchmark/stateful/bloatnet/stubs_mainnet.json new file mode 100644 index 0000000000..2417eadd79 --- /dev/null +++ b/tests/benchmark/stateful/bloatnet/stubs_mainnet.json @@ -0,0 +1,14 @@ +{ + "test_sload_empty_erc20_balanceof_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", + "test_sload_empty_erc20_balanceof_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", + "test_sload_empty_erc20_balanceof_IMT": "0x13119e34e140097a507b07a5564bde1bc375d9e6", + "test_sstore_erc20_approve_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", + "test_sstore_erc20_approve_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", + "test_sstore_erc20_approve_IMT": "0x13119e34e140097a507b07a5564bde1bc375d9e6", + "test_mixed_sload_sstore_XEN": "0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8", + "test_mixed_sload_sstore_USDC": "0xA0b86991C6218B36c1d19D4a2E9Eb0CE3606EB48", + "test_mixed_sload_sstore_IMT": "0x13119e34e140097a507b07a5564bde1bc375d9e6", + "test_sload_empty_erc20_balanceof_STR": "0x7c4d13a8e743b036f6ba71c5dcadfe4fc9aa7a17", + "test_sstore_erc20_approve_STR": "0x7c4d13a8e743b036f6ba71c5dcadfe4fc9aa7a17", + "test_mixed_sload_sstore_STR": "0x7c4d13a8e743b036f6ba71c5dcadfe4fc9aa7a17" +} diff --git a/tests/benchmark/stateful/bloatnet/test_multi_opcode.py b/tests/benchmark/stateful/bloatnet/test_multi_opcode.py index 0521462066..7b48ee5a8d 100755 --- a/tests/benchmark/stateful/bloatnet/test_multi_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_multi_opcode.py @@ -594,7 +594,7 @@ def test_bloatnet_balance_extcodehash( APPROVE_SELECTOR = 0x095EA7B3 # approve(address,uint256) # Load token names from stubs.json for test parametrization -_STUBS_FILE = Path(__file__).parent / "stubs.json" +_STUBS_FILE = Path(__file__).parent / "stubs_bloatnet.json" with open(_STUBS_FILE) as f: _STUBS = json.load(f) diff --git a/tests/benchmark/stateful/bloatnet/test_single_opcode.py b/tests/benchmark/stateful/bloatnet/test_single_opcode.py index ef513743b7..ecff876acd 100644 --- a/tests/benchmark/stateful/bloatnet/test_single_opcode.py +++ b/tests/benchmark/stateful/bloatnet/test_single_opcode.py @@ -45,7 +45,7 @@ ALLOWANCE_SELECTOR = 0xDD62ED3E # allowance(address,address) # Load token names from stubs.json for test parametrization -_STUBS_FILE = Path(__file__).parent / "stubs.json" +_STUBS_FILE = Path(__file__).parent / "stubs_bloatnet.json" with open(_STUBS_FILE) as f: _STUBS = json.load(f) diff --git a/tests/berlin/eip2929_gas_cost_increases/test_precompile_warming.py b/tests/berlin/eip2929_gas_cost_increases/test_precompile_warming.py index 0098ebfc30..6d2e5e5490 100644 --- a/tests/berlin/eip2929_gas_cost_increases/test_precompile_warming.py +++ b/tests/berlin/eip2929_gas_cost_increases/test_precompile_warming.py @@ -161,14 +161,12 @@ def test_precompile_warming( successor = get_transition_fork_successor(fork) def get_expected_gas(precompile_present: bool, fork: Fork) -> int: - gas_costs = fork.gas_costs() - warm_access_cost = gas_costs.G_WARM_ACCOUNT_ACCESS - cold_access_cost = gas_costs.G_COLD_ACCOUNT_ACCESS - extra_cost = gas_costs.G_BASE * 2 + gas_costs.G_VERY_LOW - if precompile_present: - return warm_access_cost + extra_cost - else: - return cold_access_cost + extra_cost + balance_cost = Op.BALANCE(address_warm=precompile_present).gas_cost( + fork + ) + # Overhead: GAS + POP + SUB + overhead_cost = (Op.GAS + Op.POP + Op.SUB).gas_cost(fork) + return balance_cost + overhead_cost expected_gas_before = get_expected_gas( precompile_in_predecessor, predecessor diff --git a/tests/berlin/eip2930_access_list/test_acl.py b/tests/berlin/eip2930_access_list/test_acl.py index d96c520537..71116c9074 100644 --- a/tests/berlin/eip2930_access_list/test_acl.py +++ b/tests/berlin/eip2930_access_list/test_acl.py @@ -42,15 +42,15 @@ def test_account_storage_warm_cold_state( ) -> None: """Test type 1 transaction.""" env = Environment() - gas_costs = fork.gas_costs() storage_reader_contract = pre.deploy_contract(Op.SLOAD(1) + Op.STOP) + # Overhead: PUSH args for CALL (popped_stack_items - 1) + # + GAS opcode + PUSH for SLOAD overhead_cost = ( - gas_costs.G_VERY_LOW - * (Op.CALL.popped_stack_items - 1) # Call stack items - + gas_costs.G_BASE # Call gas - + gas_costs.G_VERY_LOW # SLOAD Push - ) + Op.PUSH1(0) * (Op.CALL.popped_stack_items - 1) + + Op.GAS + + Op.PUSH1(0) # SLOAD push + ).gas_cost(fork) contract_address = pre.deploy_contract( CodeGasMeasure( code=Op.CALL(address=storage_reader_contract), @@ -59,19 +59,16 @@ def test_account_storage_warm_cold_state( sstore_key=0, ) ) - expected_gas_cost = 0 access_list_address = Address(0) access_list_storage_key = Hash(0) + # Expected gas: CALL access cost + SLOAD cost + expected_gas_cost = Op.CALL(address_warm=account_warm).gas_cost( + fork + ) + Op.SLOAD(key_warm=storage_key_warm).gas_cost(fork) if account_warm: - expected_gas_cost += gas_costs.G_WARM_ACCOUNT_ACCESS access_list_address = storage_reader_contract - else: - expected_gas_cost += gas_costs.G_COLD_ACCOUNT_ACCESS if storage_key_warm: - expected_gas_cost += gas_costs.G_WARM_SLOAD access_list_storage_key = Hash(1) - else: - expected_gas_cost += gas_costs.G_COLD_SLOAD access_lists: List[AccessList] = [ AccessList( @@ -233,7 +230,6 @@ def test_transaction_intrinsic_gas_cost( ) sender = pre.fund_eoa() tx_value = 1 - pre.fund_address(sender, tx_value) contract_creation = False tx_data = b"" @@ -289,12 +285,13 @@ def test_repeated_address_acl( of each access in order to make debugging easier. """ sender = pre.fund_eoa() - gsc = fork.gas_costs() + + # Cost of pushing SLOAD args + sload_push_cost = (Op.PUSH1(0) * len(Op.SLOAD.kwargs)).gas_cost(fork) sload0_measure = CodeGasMeasure( code=Op.SLOAD(0), - overhead_cost=gsc.G_VERY_LOW - * len(Op.SLOAD.kwargs), # Cost of pushing SLOAD args + overhead_cost=sload_push_cost, extra_stack_items=1, # SLOAD pushes 1 item to the stack sstore_key=0, stop=False, # Because it's the first CodeGasMeasure @@ -302,8 +299,7 @@ def test_repeated_address_acl( sload1_measure = CodeGasMeasure( code=Op.SLOAD(1), - overhead_cost=gsc.G_VERY_LOW - * len(Op.SLOAD.kwargs), # Cost of pushing SLOAD args + overhead_cost=sload_push_cost, extra_stack_items=1, # SLOAD pushes 1 item to the stack sstore_key=1, ) @@ -327,7 +323,7 @@ def test_repeated_address_acl( ], ) - sload_cost = gsc.G_WARM_ACCOUNT_ACCESS + sload_cost = Op.SLOAD(key_warm=True).gas_cost(fork) state_test( env=Environment(), diff --git a/tests/cancun/eip1153_tstore/test_basic_tload.py b/tests/cancun/eip1153_tstore/test_basic_tload.py index 70ca668553..6cd9eddd64 100644 --- a/tests/cancun/eip1153_tstore/test_basic_tload.py +++ b/tests/cancun/eip1153_tstore/test_basic_tload.py @@ -11,12 +11,13 @@ Address, Alloc, Environment, + Fork, Op, StateTestFiller, Transaction, ) -from .spec import Spec, ref_spec_1153 +from .spec import ref_spec_1153 REFERENCE_SPEC_GIT_PATH = ref_spec_1153.git_path REFERENCE_SPEC_VERSION = ref_spec_1153.version @@ -195,6 +196,7 @@ def test_basic_tload_other_after_tstore( def test_basic_tload_gasprice( state_test: StateTestFiller, pre: Alloc, + fork: Fork, ) -> None: """ Ported .json vectors. @@ -261,8 +263,8 @@ def test_basic_tload_gasprice( post = { address_to: Account( storage={ - slot_tload_nonzero_gas_price_result: Spec.TLOAD_GAS_COST, - slot_tload_zero_gas_price_result: Spec.TLOAD_GAS_COST, + slot_tload_nonzero_gas_price_result: Op.TLOAD.gas_cost(fork), + slot_tload_zero_gas_price_result: Op.TLOAD.gas_cost(fork), slot_code_worked: 0x01, } ) diff --git a/tests/cancun/eip1153_tstore/test_tstorage.py b/tests/cancun/eip1153_tstore/test_tstorage.py index c77e6ba638..2f5db22639 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage.py +++ b/tests/cancun/eip1153_tstore/test_tstorage.py @@ -21,7 +21,7 @@ ) from . import PytestParameterEnum -from .spec import Spec, ref_spec_1153 +from .spec import ref_spec_1153 REFERENCE_SPEC_GIT_PATH = ref_spec_1153.git_path REFERENCE_SPEC_VERSION = ref_spec_1153.version @@ -217,33 +217,25 @@ class GasMeasureTestCases(PytestParameterEnum): "description": "Test that tload() of an empty slot consumes " "the expected gas.", "bytecode": Op.TLOAD(10), - "overhead_cost": 3, # 1 x PUSH1 "extra_stack_items": 1, - "expected_gas": Spec.TLOAD_GAS_COST, } TSTORE_TLOAD = { "description": "Test that tload() of a used slot consumes " "the expected gas.", "bytecode": Op.TSTORE(10, 10) + Op.TLOAD(10), - "overhead_cost": 3 * 3, # 3 x PUSH1 "extra_stack_items": 1, - "expected_gas": Spec.TSTORE_GAS_COST + Spec.TLOAD_GAS_COST, } TSTORE_COLD = { "description": "Test that tstore() of a previously unused " "slot consumes the expected gas.", "bytecode": Op.TSTORE(10, 10), - "overhead_cost": 2 * 3, # 2 x PUSH1 "extra_stack_items": 0, - "expected_gas": Spec.TSTORE_GAS_COST, } TSTORE_WARM = { "description": "Test that tstore() of a previously used slot " "consumes the expected gas.", "bytecode": Op.TSTORE(10, 10) + Op.TSTORE(10, 11), - "overhead_cost": 4 * 3, # 4 x PUSH1 "extra_stack_items": 0, - "expected_gas": 2 * Spec.TSTORE_GAS_COST, } @@ -251,15 +243,14 @@ class GasMeasureTestCases(PytestParameterEnum): def test_gas_usage( state_test: StateTestFiller, pre: Alloc, + fork: Fork, bytecode: Bytecode, - expected_gas: int, - overhead_cost: int, extra_stack_items: int, ) -> None: """Test that tstore and tload consume the expected gas.""" + expected_gas = bytecode.gas_cost(fork) gas_measure_bytecode = CodeGasMeasure( code=bytecode, - overhead_cost=overhead_cost, extra_stack_items=extra_stack_items, ) diff --git a/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py b/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py index f8850f50c4..b81307b742 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_clear_after_tx.py @@ -11,6 +11,7 @@ Op, Transaction, ) +from execution_testing.forks.helpers import Fork from .spec import ref_spec_1153 @@ -22,6 +23,7 @@ def test_tstore_clear_after_deployment_tx( blockchain_test: BlockchainTestFiller, pre: Alloc, + fork: Fork, ) -> None: """ First creates a contract, which TSTOREs a value 1 in slot 1. After creating @@ -34,7 +36,9 @@ def test_tstore_clear_after_deployment_tx( init_code = Op.TSTORE(1, 1) deploy_code = Op.SSTORE(1, Op.TLOAD(1)) - code = Initcode(deploy_code=deploy_code, initcode_prefix=init_code) + code = Initcode( + deploy_code=deploy_code, initcode_prefix=init_code, fork=fork + ) sender = pre.fund_eoa() diff --git a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py index 6161c2caa0..7abb7c373b 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_create_contexts.py @@ -17,6 +17,7 @@ Transaction, compute_create_address, ) +from execution_testing.forks.helpers import Fork from . import CreateOpcodeParams, PytestParameterEnum from .spec import ref_spec_1153 @@ -164,9 +165,12 @@ def initcode( # noqa: D102 self, deploy_code: Bytecode, constructor_code: Bytecode, + fork: Fork, ) -> Initcode: return Initcode( - deploy_code=deploy_code, initcode_prefix=constructor_code + deploy_code=deploy_code, + initcode_prefix=constructor_code, + fork=fork, ) @pytest.fixture() diff --git a/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py b/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py index 59c7778239..f35a2ed53f 100644 --- a/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py +++ b/tests/cancun/eip1153_tstore/test_tstorage_execution_contexts.py @@ -3,7 +3,7 @@ """ from enum import EnumMeta, unique -from typing import Any, Dict, Mapping +from typing import Any, Callable, Dict, Mapping import pytest from execution_testing import ( @@ -12,6 +12,7 @@ Alloc, Bytecode, Environment, + Fork, Hash, Op, StateTestFiller, @@ -19,15 +20,13 @@ ) from . import PytestParameterEnum -from .spec import Spec, ref_spec_1153 +from .spec import ref_spec_1153 REFERENCE_SPEC_GIT_PATH = ref_spec_1153.git_path REFERENCE_SPEC_VERSION = ref_spec_1153.version pytestmark = [pytest.mark.valid_from("Cancun")] -PUSH_OPCODE_COST = 3 - class DynamicCallContextTestCases(EnumMeta): """ @@ -186,25 +185,30 @@ def __new__( # noqa: D102 "expected_callee_storage": {}, } - gas_limit = Spec.TSTORE_GAS_COST + (PUSH_OPCODE_COST * 2) - 1 - contract_call = call_opcode( - gas=gas_limit, address=Op.CALLDATALOAD(0) - ) + callee_bytecode = Op.TSTORE(1, 69) + Op.STOP classdict[f"{call_opcode._name_}_WITH_OUT_OF_GAS"] = { "description": ( "Transient storage usage is discarded from sub-call with " f"{call_opcode._name_} upon out of gas during TSTORE. " "Note: Gas passed to sub-call is capped." ), - "caller_bytecode": ( + "caller_bytecode": lambda fork, + call_opcode=call_opcode, + callee_bytecode=callee_bytecode: ( Op.TSTORE(0, 420) + Op.TSTORE(1, 420) - + Op.SSTORE(0, contract_call) + + Op.SSTORE( + 0, + call_opcode( + gas=callee_bytecode.gas_cost(fork) - 1, + address=Op.CALLDATALOAD(0), + ), + ) + Op.SSTORE(1, Op.TLOAD(0)) + Op.SSTORE(2, Op.TLOAD(1)) + Op.STOP ), - "callee_bytecode": Op.TSTORE(1, 69) + Op.STOP, + "callee_bytecode": callee_bytecode, "expected_caller_storage": {0: 0, 1: 420, 2: 420}, "expected_callee_storage": {}, } @@ -320,8 +324,15 @@ def __init__(self, value: dict[str, Any]) -> None: @pytest.fixture() -def caller_address(pre: Alloc, caller_bytecode: Bytecode) -> Address: +def caller_address( + pre: Alloc, + fork: Fork, + caller_bytecode: Bytecode | Callable[[Fork], Bytecode], +) -> Address: """Address used to call the test bytecode on every test case.""" + if not isinstance(caller_bytecode, Bytecode): + assert callable(caller_bytecode) + caller_bytecode = caller_bytecode(fork) return pre.deploy_contract(caller_bytecode) diff --git a/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py b/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py index 4225ddb949..3683785594 100644 --- a/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py +++ b/tests/cancun/eip4788_beacon_root/test_beacon_root_contract.py @@ -49,12 +49,6 @@ def count_factory(start: int, step: int = 1) -> Callable[[], Iterator[int]]: return lambda: count(start, step) -pytestmark = pytest.mark.pre_alloc_group( - "beacon_root_tests", - reason="Tests beacon root contract functionality using system contract", -) - - @pytest.mark.parametrize( "call_gas, valid_call", [ @@ -130,30 +124,9 @@ def test_beacon_root_contract_calls( @pytest.mark.parametrize( "system_address_balance", [ - pytest.param( - 0, - id="empty_system_address", - marks=pytest.mark.pre_alloc_group( - "beacon_root_empty_system", - reason="Tests with empty system address balance", - ), - ), - pytest.param( - 1, - id="one_wei_system_address", - marks=pytest.mark.pre_alloc_group( - "beacon_root_one_wei_system", - reason="Tests with 1 wei system address balance", - ), - ), - pytest.param( - int(1e18), - id="one_eth_system_address", - marks=pytest.mark.pre_alloc_group( - "beacon_root_one_eth_system", - reason="Tests with 1 ETH system address balance", - ), - ), + pytest.param(0, id="empty_system_address"), + pytest.param(1, id="one_wei_system_address"), + pytest.param(int(1e18), id="one_eth_system_address"), ], ) @pytest.mark.valid_from("Cancun") @@ -697,10 +670,7 @@ def test_beacon_root_transition( @pytest.mark.parametrize("timestamp", [15_000]) @pytest.mark.valid_at_transition_to("Cancun") -@pytest.mark.pre_alloc_group( - "beacon_root_no_contract", - reason="This test removes the beacon root system contract", -) +@pytest.mark.pre_alloc_mutable() def test_no_beacon_root_contract_at_transition( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -779,14 +749,7 @@ def test_no_beacon_root_contract_at_transition( ], ) @pytest.mark.valid_at_transition_to("Cancun") -@pytest.mark.pre_alloc_group( - "beacon_root_deploy_contract", - reason=( - "This test is parametrized with a hard-coded address (the beacon root " - "contract deployer address); they can't be in the same pre alloc " - "group." - ), -) +@pytest.mark.pre_alloc_mutable() def test_beacon_root_contract_deploy( blockchain_test: BlockchainTestFiller, pre: Alloc, diff --git a/tests/cancun/eip4844_blobs/test_blobhash_opcode.py b/tests/cancun/eip4844_blobs/test_blobhash_opcode.py index 6cc1c55a11..2040430157 100644 --- a/tests/cancun/eip4844_blobs/test_blobhash_opcode.py +++ b/tests/cancun/eip4844_blobs/test_blobhash_opcode.py @@ -188,9 +188,9 @@ def test_blobhash_gas_cost( ensuring it matches `HASH_OPCODE_GAS = 3`. Includes both valid and invalid random index sizes from the range `[0, 2**256-1]`, for tx types 2 and 3. """ + blobhash_code = Op.BLOBHASH(blobhash_index) gas_measure_code = CodeGasMeasure( - code=Op.BLOBHASH(blobhash_index), - overhead_cost=3, + code=blobhash_code, extra_stack_items=1, ) @@ -223,7 +223,7 @@ def test_blobhash_gas_cost( ] tx = Transaction(**tx_kwargs) - post = {address: Account(storage={0: Spec.HASH_GAS_COST})} + post = {address: Account(storage={0: blobhash_code.gas_cost(fork)})} state_test( env=Environment(), diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py index a46cc06314..72047384d3 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile.py @@ -105,14 +105,14 @@ def call_opcode() -> Op: @pytest.fixture -def call_gas() -> int: +def call_gas(fork: Fork) -> int: """ Amount of gas to pass to the precompile. - Defaults to Spec.POINT_EVALUATION_PRECOMPILE_GAS, but can be parametrized - to test different amounts. + Defaults to the point evaluation precompile gas cost, but can be + parametrized to test different amounts. """ - return Spec.POINT_EVALUATION_PRECOMPILE_GAS + return fork.gas_costs().G_PRECOMPILE_POINT_EVALUATION precompile_caller_storage_keys = count() @@ -194,13 +194,14 @@ def tx( precompile_caller_address: Address, precompile_input: bytes, sender: EOA, + fork: Fork, ) -> Transaction: """Prepare transaction used to call the precompile caller account.""" return Transaction( sender=sender, data=precompile_input, to=precompile_caller_address, - gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 100, + gas_limit=fork.gas_costs().G_PRECOMPILE_POINT_EVALUATION * 100, ) @@ -583,9 +584,10 @@ def test_tx_entry_point( # Consumed gas will only be the precompile gas if the proof is correct and # the call gas is sufficient. # Otherwise, the call gas will be consumed in full. + precompile_gas = fork.gas_costs().G_PRECOMPILE_POINT_EVALUATION consumed_gas = ( - Spec.POINT_EVALUATION_PRECOMPILE_GAS - if call_gas >= Spec.POINT_EVALUATION_PRECOMPILE_GAS and proof_correct + precompile_gas + if call_gas >= precompile_gas and proof_correct else call_gas ) + tx_intrinsic_gas_cost_calculator( calldata=precompile_input, @@ -701,10 +703,12 @@ def test_precompile_during_fork( precompile_caller_address: Address, precompile_input: bytes, sender: EOA, + fork: Fork, ) -> None: """ Test calling the Point Evaluation Precompile during the appropriate fork. """ + precompile_gas = fork.gas_costs().G_PRECOMPILE_POINT_EVALUATION # Blocks before fork blocks = [ Block( @@ -714,7 +718,7 @@ def test_precompile_during_fork( sender=sender, data=precompile_input, to=precompile_caller_address, - gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 100, + gas_limit=precompile_gas * 100, ) ], ) @@ -729,7 +733,7 @@ def test_precompile_during_fork( sender=sender, data=precompile_input, to=precompile_caller_address, - gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 100, + gas_limit=precompile_gas * 100, ) ], ) diff --git a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py index 1ae309cda4..12c275e295 100644 --- a/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py +++ b/tests/cancun/eip4844_blobs/test_point_evaluation_precompile_gas.py @@ -59,14 +59,14 @@ def call_type() -> Op: @pytest.fixture -def call_gas() -> int: +def call_gas(fork: Fork) -> int: """ Amount of gas to pass to the precompile. - Defaults to POINT_EVALUATION_PRECOMPILE_GAS, but can be parametrized to - test different amounts. + Defaults to the point evaluation precompile gas cost, but can be + parametrized to test different amounts. """ - return Spec.POINT_EVALUATION_PRECOMPILE_GAS + return fork.gas_costs().G_PRECOMPILE_POINT_EVALUATION def copy_opcode_cost(fork: Fork, length: int) -> int: @@ -149,6 +149,7 @@ def tx( pre: Alloc, precompile_caller_address: Address, precompile_input: bytes, + fork: Fork, ) -> Transaction: """Prepare transaction used to call the precompile caller account.""" return Transaction( @@ -156,7 +157,7 @@ def tx( data=precompile_input, to=precompile_caller_address, value=0, - gas_limit=Spec.POINT_EVALUATION_PRECOMPILE_GAS * 20, + gas_limit=fork.gas_costs().G_PRECOMPILE_POINT_EVALUATION * 20, ) @@ -165,16 +166,16 @@ def post( precompile_caller_address: Address, proof: Literal["correct", "incorrect"], call_gas: int, + fork: Fork, ) -> Dict: """ Prepare expected post for each test, depending on the success or failure of the precompile call and the gas usage. """ + precompile_gas = fork.gas_costs().G_PRECOMPILE_POINT_EVALUATION if proof == "correct": expected_gas_usage = ( - call_gas - if call_gas < Spec.POINT_EVALUATION_PRECOMPILE_GAS - else Spec.POINT_EVALUATION_PRECOMPILE_GAS + call_gas if call_gas < precompile_gas else precompile_gas ) else: expected_gas_usage = call_gas diff --git a/tests/cancun/eip6780_selfdestruct/test_dynamic_create2_selfdestruct_collision.py b/tests/cancun/eip6780_selfdestruct/test_dynamic_create2_selfdestruct_collision.py index d27b12730c..a695a0e23b 100644 --- a/tests/cancun/eip6780_selfdestruct/test_dynamic_create2_selfdestruct_collision.py +++ b/tests/cancun/eip6780_selfdestruct/test_dynamic_create2_selfdestruct_collision.py @@ -31,9 +31,7 @@ @pytest.mark.parametrize( "create2_dest_already_in_state", ( - pytest.param( - True, marks=pytest.mark.execute(pytest.mark.skip("Modifies pre")) - ), + pytest.param(True, marks=pytest.mark.pre_alloc_mutable), False, ), ) @@ -266,9 +264,7 @@ def test_dynamic_create2_selfdestruct_collision( @pytest.mark.parametrize( "create2_dest_already_in_state", ( - pytest.param( - True, marks=pytest.mark.execute(pytest.mark.skip("Modifies pre")) - ), + pytest.param(True, marks=pytest.mark.pre_alloc_mutable), False, ), ) diff --git a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py index 2794c41957..731e920320 100644 --- a/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py +++ b/tests/cancun/eip6780_selfdestruct/test_selfdestruct.py @@ -188,7 +188,10 @@ def selfdestruct_code( ], indirect=["sendall_recipient_addresses"], ) -@pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 100_000]) +@pytest.mark.parametrize( + "selfdestruct_contract_initial_balance", + [0, 100_000], +) @pytest.mark.valid_from("Shanghai") def test_create_selfdestruct_same_tx( state_test: StateTestFiller, @@ -359,7 +362,10 @@ def test_create_selfdestruct_same_tx( @pytest.mark.parametrize("create_opcode", [Op.CREATE, Op.CREATE2]) @pytest.mark.parametrize("call_times", [0, 1]) -@pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 100_000]) +@pytest.mark.parametrize( + "selfdestruct_contract_initial_balance", + [0, 100_000], +) @pytest.mark.valid_from("Shanghai") def test_self_destructing_initcode( state_test: StateTestFiller, @@ -489,7 +495,10 @@ def test_self_destructing_initcode( @pytest.mark.parametrize("tx_value", [0, 100_000]) -@pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 100_000]) +@pytest.mark.parametrize( + "selfdestruct_contract_initial_balance", + [0, 100_000], +) @pytest.mark.valid_from("Shanghai") def test_self_destructing_initcode_create_tx( state_test: StateTestFiller, @@ -518,9 +527,11 @@ def test_self_destructing_initcode_create_tx( gas_limit=500_000 + fork_extra_gas, ) selfdestruct_contract_address = tx.created_contract - pre.fund_address( - selfdestruct_contract_address, selfdestruct_contract_initial_balance - ) + if selfdestruct_contract_initial_balance > 0: + pre.fund_address( + selfdestruct_contract_address, + selfdestruct_contract_initial_balance, + ) # Our entry point is an initcode that in turn creates a self-destructing # contract @@ -552,7 +563,10 @@ def test_self_destructing_initcode_create_tx( ], indirect=["sendall_recipient_addresses"], ) -@pytest.mark.parametrize("selfdestruct_contract_initial_balance", [0, 100_000]) +@pytest.mark.parametrize( + "selfdestruct_contract_initial_balance", + [0, 100_000], +) @pytest.mark.parametrize("recreate_times", [1]) @pytest.mark.parametrize("call_times", [1]) @pytest.mark.valid_from("Shanghai") @@ -630,9 +644,11 @@ def test_recreate_self_destructed_contract_different_txs( initcode=selfdestruct_contract_initcode, opcode=create_opcode, ) - pre.fund_address( - selfdestruct_contract_address, selfdestruct_contract_initial_balance - ) + if selfdestruct_contract_initial_balance > 0: + pre.fund_address( + selfdestruct_contract_address, + selfdestruct_contract_initial_balance, + ) for i in range(len(sendall_recipient_addresses)): if sendall_recipient_addresses[i] == SELF_ADDRESS: sendall_recipient_addresses[i] = selfdestruct_contract_address @@ -1005,9 +1021,11 @@ def test_calling_from_new_contract_to_pre_existing_contract( address=entry_code_address, nonce=1 ) - pre.fund_address( - selfdestruct_contract_address, selfdestruct_contract_initial_balance - ) + if selfdestruct_contract_initial_balance > 0: + pre.fund_address( + selfdestruct_contract_address, + selfdestruct_contract_initial_balance, + ) # self-destructing call selfdestruct_code = call_opcode(address=pre_existing_selfdestruct_address) diff --git a/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py b/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py index d053c96442..ca483e637d 100644 --- a/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py +++ b/tests/cancun/eip7516_blobgasfee/test_blobgasfee_opcode.py @@ -15,6 +15,7 @@ BlockchainTestFiller, Bytecode, Environment, + Fork, Op, StateTestFiller, Storage, @@ -24,8 +25,6 @@ REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7516.md" REFERENCE_SPEC_VERSION = "dcd2f4ede58a6ed908acd3cc2c198e9f605cbf3b" -BLOBBASEFEE_GAS = 2 - @pytest.fixture def call_gas() -> int: @@ -127,22 +126,39 @@ def test_blobbasefee_stack_overflow( @pytest.mark.parametrize( - "call_gas,call_fails", + "call_fails", [ - pytest.param(BLOBBASEFEE_GAS, False, id="enough_gas"), - pytest.param(BLOBBASEFEE_GAS - 1, True, id="out_of_gas"), + pytest.param(False, id="enough_gas"), + pytest.param(True, id="out_of_gas"), ], ) @pytest.mark.valid_from("Cancun") def test_blobbasefee_out_of_gas( state_test: StateTestFiller, pre: Alloc, - caller_address: Address, - callee_address: Address, - tx: Transaction, + fork: Fork, call_fails: bool, ) -> None: """Tests that the BLOBBASEFEE opcode fails with insufficient gas.""" + blobbasefee_gas = Op.BLOBBASEFEE.gas_cost(fork) + call_gas = blobbasefee_gas - 1 if call_fails else blobbasefee_gas + + callee_code = Op.BLOBBASEFEE + Op.STOP + callee_address = pre.deploy_contract(callee_code) + + caller_code = Op.SSTORE( + Op.SELFBALANCE, + Op.CALL(gas=call_gas, address=callee_address), + ) + caller_address = pre.deploy_contract(caller_code) + + tx = Transaction( + sender=pre.fund_eoa(), + gas_limit=1_000_000, + to=caller_address, + value=1, + ) + post = { caller_address: Account( storage={1: 0 if call_fails else 1}, diff --git a/tests/frontier/opcodes/test_call.py b/tests/frontier/opcodes/test_call.py index 59ef2559f6..1eba27f574 100644 --- a/tests/frontier/opcodes/test_call.py +++ b/tests/frontier/opcodes/test_call.py @@ -31,21 +31,22 @@ def test_call_large_offset_mstore( """ sender = pre.fund_eoa() - gsc = fork.gas_costs() mem_offset = 128 # arbitrary number + # Cost of pushing args onto the stack (each PUSH costs G_VERY_LOW) + call_push_cost = (Op.PUSH1(0) * len(Op.CALL.kwargs)).gas_cost(fork) + mstore_push_cost = (Op.PUSH1(0) * len(Op.MSTORE.kwargs)).gas_cost(fork) + call_measure = CodeGasMeasure( code=Op.CALL(gas=0, ret_offset=mem_offset, ret_size=0), - # Cost of pushing CALL args - overhead_cost=gsc.G_VERY_LOW * len(Op.CALL.kwargs), + overhead_cost=call_push_cost, extra_stack_items=1, # Because CALL pushes 1 item to the stack sstore_key=0, stop=False, # Because it's the first CodeGasMeasure ) mstore_measure = CodeGasMeasure( code=Op.MSTORE(offset=mem_offset, value=1), - # Cost of pushing MSTORE args - overhead_cost=gsc.G_VERY_LOW * len(Op.MSTORE.kwargs), + overhead_cost=mstore_push_cost, extra_stack_items=0, sstore_key=1, ) @@ -60,13 +61,10 @@ def test_call_large_offset_mstore( ) # this call cost is just the address_access_cost - call_cost = gsc.G_COLD_ACCOUNT_ACCESS + call_cost = Op.CALL(address_warm=False).gas_cost(fork) - memory_expansion_gas_calc = fork.memory_expansion_gas_calculator() # mstore cost: base cost + expansion cost - mstore_cost = gsc.G_MEMORY + memory_expansion_gas_calc( - new_bytes=mem_offset + 1 - ) + mstore_cost = Op.MSTORE(new_memory_size=mem_offset + 32).gas_cost(fork) state_test( env=Environment(), pre=pre, @@ -100,15 +98,17 @@ def test_call_memory_expands_on_early_revert( """ sender = pre.fund_eoa() - gsc = fork.gas_costs() # arbitrary number, greater than memory size to trigger an expansion ret_size = 128 + # Cost of pushing args onto the stack (each PUSH costs G_VERY_LOW) + call_push_cost = (Op.PUSH1(0) * len(Op.CALL.kwargs)).gas_cost(fork) + mstore_push_cost = (Op.PUSH1(0) * len(Op.MSTORE.kwargs)).gas_cost(fork) + call_measure = CodeGasMeasure( # CALL with value code=Op.CALL(gas=0, value=100, ret_size=ret_size), - # Cost of pushing CALL args - overhead_cost=gsc.G_VERY_LOW * len(Op.CALL.kwargs), + overhead_cost=call_push_cost, # Because CALL pushes 1 item to the stack extra_stack_items=1, sstore_key=0, @@ -118,8 +118,7 @@ def test_call_memory_expands_on_early_revert( mstore_measure = CodeGasMeasure( # Low offset for not expanding memory code=Op.MSTORE(offset=ret_size // 2, value=1), - # Cost of pushing MSTORE args - overhead_cost=gsc.G_VERY_LOW * len(Op.MSTORE.kwargs), + overhead_cost=mstore_push_cost, extra_stack_items=0, sstore_key=1, ) @@ -136,20 +135,23 @@ def test_call_memory_expands_on_early_revert( sender=sender, ) - memory_expansion_gas_calc = fork.memory_expansion_gas_calculator() # call cost: # address_access_cost+new_acc_cost+memory_expansion_cost+value-stipend + # G_CALL_STIPEND is a threshold check, not a gas cost — keep from gas_costs + gsc = fork.gas_costs() call_cost = ( - gsc.G_COLD_ACCOUNT_ACCESS - + gsc.G_NEW_ACCOUNT - + memory_expansion_gas_calc(new_bytes=ret_size) - + gsc.G_CALL_VALUE + Op.CALL( + address_warm=False, + value_transfer=True, + account_new=True, + new_memory_size=ret_size, + ).gas_cost(fork) - gsc.G_CALL_STIPEND ) # mstore cost: base cost. No memory expansion cost needed, it was expanded # on CALL. - mstore_cost = gsc.G_MEMORY + mstore_cost = Op.MSTORE(new_memory_size=0).gas_cost(fork) state_test( env=Environment(), pre=pre, @@ -182,13 +184,14 @@ def test_call_large_args_offset_size_zero( """ sender = pre.fund_eoa() - gsc = fork.gas_costs() very_large_offset = 2**100 + # Cost of pushing args onto the stack (each PUSH costs G_VERY_LOW) + push_cost = (Op.PUSH1(0) * len(call_opcode.kwargs)).gas_cost(fork) + call_measure = CodeGasMeasure( code=call_opcode(gas=0, args_offset=very_large_offset, args_size=0), - # Cost of pushing xCALL args - overhead_cost=gsc.G_VERY_LOW * len(call_opcode.kwargs), + overhead_cost=push_cost, extra_stack_items=1, # Because xCALL pushes 1 item to the stack sstore_key=0, ) @@ -203,7 +206,7 @@ def test_call_large_args_offset_size_zero( ) # this call cost is just the address_access_cost - call_cost = gsc.G_COLD_ACCOUNT_ACCESS + call_cost = call_opcode(address_warm=False).gas_cost(fork) state_test( env=Environment(), diff --git a/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py b/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py index 3c178fb506..edc71c56e9 100644 --- a/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py +++ b/tests/frontier/opcodes/test_call_and_callcode_gas_calculation.py @@ -66,10 +66,12 @@ def callee_init_stack_gas(callee_opcode: Op, fork: Fork) -> int: """ if fork < Byzantium: # all *CALL arguments handled with PUSHes - return len(callee_opcode.kwargs) * 3 + return (Op.PUSH1(0) * len(callee_opcode.kwargs)).gas_cost(fork) else: # gas argument handled with GAS which is cheaper - return (len(callee_opcode.kwargs) - 1) * 3 + 2 + return ( + Op.PUSH1(0) * (len(callee_opcode.kwargs) - 1) + Op.GAS + ).gas_cost(fork) @pytest.fixture @@ -80,30 +82,33 @@ def sufficient_gas( Calculate the sufficient gas for the nested call opcode with positive value transfer. """ - gas_costs = fork.gas_costs() - - cost = 0 + is_value_call = callee_opcode in [Op.CALL, Op.CALLCODE] if fork >= Berlin: - cost += gas_costs.G_COLD_ACCOUNT_ACCESS + metadata: dict = {"address_warm": False} + if is_value_call: + metadata["value_transfer"] = True + metadata["account_new"] = callee_opcode == Op.CALL + cost = callee_opcode(**metadata).gas_cost(fork) elif Byzantium <= fork < Berlin: - cost += 700 # Pre-Berlin warm call cost + cost = 700 # Pre-Berlin call cost + gas_costs = fork.gas_costs() + if is_value_call: + cost += gas_costs.G_CALL_VALUE + if callee_opcode == Op.CALL: + cost += gas_costs.G_NEW_ACCOUNT elif fork == Homestead: - cost += 40 # Homestead call cost + cost = 40 # Homestead call cost cost += 1 # mandatory callee gas allowance + gas_costs = fork.gas_costs() + if is_value_call: + cost += gas_costs.G_CALL_VALUE + if callee_opcode == Op.CALL: + cost += gas_costs.G_NEW_ACCOUNT else: raise Exception("Only forks Homestead and >=Byzantium supported") - is_value_call = callee_opcode in [Op.CALL, Op.CALLCODE] - if is_value_call: - cost += gas_costs.G_CALL_VALUE - - if callee_opcode == Op.CALL: - cost += gas_costs.G_NEW_ACCOUNT - - sufficient = callee_init_stack_gas + cost - - return sufficient + return callee_init_stack_gas + cost @pytest.fixture diff --git a/tests/frontier/validation/test_transaction.py b/tests/frontier/validation/test_transaction.py index 33fd69ff13..2f10ce2bfe 100644 --- a/tests/frontier/validation/test_transaction.py +++ b/tests/frontier/validation/test_transaction.py @@ -64,6 +64,7 @@ def test_tx_gas_limit( ), ], ) +@pytest.mark.pre_alloc_mutable def test_tx_nonce( blockchain_test: BlockchainTestFiller, pre: Alloc, diff --git a/tests/istanbul/eip152_blake2/test_blake2.py b/tests/istanbul/eip152_blake2/test_blake2.py index 36c70c77e4..ac9f2eb171 100644 --- a/tests/istanbul/eip152_blake2/test_blake2.py +++ b/tests/istanbul/eip152_blake2/test_blake2.py @@ -409,7 +409,7 @@ def test_blake2b( gas_costs = fork.gas_costs() sstore_cost = gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD - blake_cost = data.rounds * gas_costs.G_BLAKE2_PER_ROUND + blake_cost = data.rounds * gas_costs.G_PRECOMPILE_BLAKE2F_PER_ROUND data_bytes = data.create_blake2b_tx_data() diff --git a/tests/osaka/eip7823_modexp_upper_bounds/conftest.py b/tests/osaka/eip7823_modexp_upper_bounds/conftest.py index f9528d8e92..461537c904 100644 --- a/tests/osaka/eip7823_modexp_upper_bounds/conftest.py +++ b/tests/osaka/eip7823_modexp_upper_bounds/conftest.py @@ -87,12 +87,18 @@ def gas_measure_contract( 0, ) - gas_costs = fork.gas_costs() extra_gas = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - + (gas_costs.G_VERY_LOW * (len(Op.CALL.kwargs) - 1)) - + gas_costs.G_BASE # CALLDATASIZE - + gas_costs.G_BASE # GAS + Op.CALL( + precompile_gas, + Spec.MODEXP_ADDRESS, + 0, + 0, + Op.CALLDATASIZE(), + 0, + 0, + address_warm=True, + ).gas_cost(fork) + + Op.GAS.gas_cost(fork) # second GAS in measurement ) # Build the gas measurement contract code diff --git a/tests/osaka/eip7825_transaction_gas_limit_cap/test_tx_gas_limit.py b/tests/osaka/eip7825_transaction_gas_limit_cap/test_tx_gas_limit.py index cb085e70d9..32085194de 100644 --- a/tests/osaka/eip7825_transaction_gas_limit_cap/test_tx_gas_limit.py +++ b/tests/osaka/eip7825_transaction_gas_limit_cap/test_tx_gas_limit.py @@ -254,8 +254,10 @@ def test_maximum_gas_refund( # Base Operation: SSTORE(slot, 0) iteration_cost = ( - gas_costs.G_STORAGE_RESET + gas_costs.G_BASE + gas_costs.G_VERY_LOW - ) + Op.SSTORE(key_warm=True, original_value=1, new_value=0) + + Op.PUSH0 + + Op.PUSH1(0) + ).gas_cost(fork) gas_refund = gas_costs.R_STORAGE_CLEAR # EIP-3529: Reduction in refunds diff --git a/tests/osaka/eip7883_modexp_gas_increase/conftest.py b/tests/osaka/eip7883_modexp_gas_increase/conftest.py index 7bdfbc5558..a94e3aa725 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/conftest.py +++ b/tests/osaka/eip7883_modexp_gas_increase/conftest.py @@ -60,9 +60,8 @@ def total_tx_gas_needed( fork.transaction_intrinsic_cost_calculator() ) memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() - sstore_gas = ( - fork.gas_costs().G_STORAGE_SET + fork.gas_costs().G_COLD_SLOAD - ) * 5 + # `gas_measure_contract` does at most 4 SSTOREs to cold slots. + sstore_gas = Op.SSTORE(key_warm=False).gas_cost(fork) * 4 # Ensures that the precompile call is not starved by the 63/64 rule. precompile_gas_with_margin = precompile_gas * 64 // 63 extra_gas = 100_000 @@ -156,12 +155,18 @@ def gas_measure_contract( 0, ) - gas_costs = fork.gas_costs() extra_gas = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - + (gas_costs.G_VERY_LOW * (len(call_opcode.kwargs) - 1)) - + gas_costs.G_BASE # CALLDATASIZE - + gas_costs.G_BASE # GAS + call_opcode( + gas_used, + Spec.MODEXP_ADDRESS, + *value, + 0, + Op.CALLDATASIZE(), + 0, + 0, + address_warm=True, + ).gas_cost(fork) + + Op.GAS.gas_cost(fork) # second GAS in measurement ) # Build the gas measurement contract code diff --git a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py index 128d1306a7..f0e87e4cbc 100644 --- a/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py +++ b/tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds_transition.py @@ -55,11 +55,13 @@ def test_modexp_fork_transition( args_size=Op.CALLDATASIZE, ) - gas_costs = fork.gas_costs() extra_gas = ( - gas_costs.G_WARM_ACCOUNT_ACCESS - + (gas_costs.G_VERY_LOW * (len(Op.CALL.kwargs) - 2)) - + (gas_costs.G_BASE * 3) + Op.CALL( + address=Spec.MODEXP_ADDRESS, + args_size=Op.CALLDATASIZE, + address_warm=True, + ).gas_cost(fork) + + Op.GAS.gas_cost(fork) # second GAS in measurement ) code = ( Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE) diff --git a/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py b/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py index 000234ed19..e5c0887fe6 100644 --- a/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py +++ b/tests/osaka/eip7934_block_rlp_limit/test_max_block_rlp_size.py @@ -35,13 +35,7 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7934.git_path REFERENCE_SPEC_VERSION = ref_spec_7934.version -pytestmark = [ - pytest.mark.pre_alloc_group( - "block_rlp_limit_tests", - reason="Block RLP size tests require exact calculations", - ), - pytest.mark.xdist_group(name="bigmem"), -] +pytestmark = pytest.mark.xdist_group(name="bigmem") HEADER_TIMESTAMP = 123456789 diff --git a/tests/osaka/eip7951_p256verify_precompiles/conftest.py b/tests/osaka/eip7951_p256verify_precompiles/conftest.py index f42b987adb..f097b4253d 100644 --- a/tests/osaka/eip7951_p256verify_precompiles/conftest.py +++ b/tests/osaka/eip7951_p256verify_precompiles/conftest.py @@ -15,8 +15,6 @@ keccak256, ) -from .spec import Spec - @pytest.fixture def vector_gas_value() -> int | None: @@ -33,14 +31,14 @@ def vector_gas_value() -> int | None: @pytest.fixture -def precompile_gas(vector_gas_value: int | None) -> int: +def precompile_gas(vector_gas_value: int | None, fork: Fork) -> int: """Gas cost for the precompile.""" + gas = fork.gas_costs().G_PRECOMPILE_P256VERIFY if vector_gas_value is not None: - assert vector_gas_value == Spec.P256VERIFY_GAS, ( - f"Calculated gas {vector_gas_value} " - f"!= Vector gas {Spec.P256VERIFY_GAS}" + assert vector_gas_value == gas, ( + f"Calculated gas {vector_gas_value} != Vector gas {gas}" ) - return Spec.P256VERIFY_GAS + return gas @pytest.fixture diff --git a/tests/osaka/eip7951_p256verify_precompiles/test_p256verify.py b/tests/osaka/eip7951_p256verify_precompiles/test_p256verify.py index 387b3e452e..4bfa1b590d 100644 --- a/tests/osaka/eip7951_p256verify_precompiles/test_p256verify.py +++ b/tests/osaka/eip7951_p256verify_precompiles/test_p256verify.py @@ -8,6 +8,7 @@ Alloc, EIPChecklist, Environment, + Fork, Op, StateTestFiller, Storage, @@ -1089,6 +1090,7 @@ def test_precompile_will_return_success_with_tx_value( input_data: bytes, expected_output: bytes, precompile_address: Address, + fork: Fork, ) -> None: """Test P256Verify precompile will not fail if value is sent.""" sender = pre.fund_eoa() @@ -1097,7 +1099,7 @@ def test_precompile_will_return_success_with_tx_value( call_256verify_bytecode = ( Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE()) + Op.CALL( - gas=Spec.P256VERIFY_GAS, + gas=fork.gas_costs().G_PRECOMPILE_P256VERIFY, address=Spec.P256VERIFY, value=Op.CALLVALUE(), args_offset=0, @@ -1230,6 +1232,7 @@ def test_contract_creation_transaction( tx: Transaction, input_data: bytes, expected_output: bytes, + fork: Fork, ) -> None: """Test the contract creation for the P256VERIFY precompile.""" sender = pre.fund_eoa() @@ -1239,7 +1242,7 @@ def test_contract_creation_transaction( contract_bytecode = ( Op.CODECOPY(0, Op.SUB(Op.CODESIZE, len(input_data)), len(input_data)) + Op.CALL( - gas=Spec.P256VERIFY_GAS, + gas=fork.gas_costs().G_PRECOMPILE_P256VERIFY, address=Spec.P256VERIFY, value=0, args_offset=0, @@ -1296,6 +1299,7 @@ def test_contract_initcode( input_data: bytes, expected_output: bytes, opcode: Op, + fork: Fork, ) -> None: """Test P256VERIFY behavior from contract creation.""" sender = pre.fund_eoa() @@ -1305,7 +1309,7 @@ def test_contract_initcode( call_256verify_bytecode = ( Op.CODECOPY(0, Op.SUB(Op.CODESIZE, len(input_data)), len(input_data)) + Op.CALL( - gas=Spec.P256VERIFY_GAS, + gas=fork.gas_costs().G_PRECOMPILE_P256VERIFY, address=Spec.P256VERIFY, value=0, args_offset=0, diff --git a/tests/paris/eip7610_create_collision/test_initcollision.py b/tests/paris/eip7610_create_collision/test_initcollision.py index 8c25f64674..bc1b2f6081 100644 --- a/tests/paris/eip7610_create_collision/test_initcollision.py +++ b/tests/paris/eip7610_create_collision/test_initcollision.py @@ -56,7 +56,7 @@ ], ), # We need to modify the pre-alloc to include the collision - pytest.mark.pre_alloc_modify, + pytest.mark.pre_alloc_mutable, ] diff --git a/tests/paris/eip7610_create_collision/test_revert_in_create.py b/tests/paris/eip7610_create_collision/test_revert_in_create.py index f8834ea81e..695a251897 100644 --- a/tests/paris/eip7610_create_collision/test_revert_in_create.py +++ b/tests/paris/eip7610_create_collision/test_revert_in_create.py @@ -20,7 +20,7 @@ pytestmark = [ pytest.mark.valid_from("Paris"), # We need to modify the pre-alloc to include the collision - pytest.mark.pre_alloc_modify, + pytest.mark.pre_alloc_mutable, ] diff --git a/tests/prague/eip2537_bls_12_381_precompiles/conftest.py b/tests/prague/eip2537_bls_12_381_precompiles/conftest.py index a339b5b50b..3c2777df3c 100644 --- a/tests/prague/eip2537_bls_12_381_precompiles/conftest.py +++ b/tests/prague/eip2537_bls_12_381_precompiles/conftest.py @@ -1,6 +1,7 @@ """Shared pytest definitions local to EIP-2537 tests.""" import pytest +from execution_testing import Fork from ...common.precompile_fixtures import ( call_contract_address, # noqa: F401 @@ -15,7 +16,7 @@ tx_gas_limit, # noqa: F401 ) from .helpers import BLSPointGenerator -from .spec import GAS_CALCULATION_FUNCTION_MAP +from .spec import build_gas_calculation_function_map @pytest.fixture @@ -34,12 +35,14 @@ def vector_gas_value() -> int | None: @pytest.fixture def precompile_gas( - precompile_address: int, input_data: bytes, vector_gas_value: int | None + precompile_address: int, + input_data: bytes, + vector_gas_value: int | None, + fork: Fork, ) -> int: """Gas cost for the precompile.""" - calculated_gas = GAS_CALCULATION_FUNCTION_MAP[precompile_address]( - len(input_data) - ) + gas_map = build_gas_calculation_function_map(fork.gas_costs()) + calculated_gas = gas_map[precompile_address](len(input_data)) if vector_gas_value is not None: assert calculated_gas == vector_gas_value, ( f"Calculated gas {calculated_gas} != Vector gas {vector_gas_value}" diff --git a/tests/prague/eip2537_bls_12_381_precompiles/spec.py b/tests/prague/eip2537_bls_12_381_precompiles/spec.py index 3a6031e95d..78b7d1affb 100644 --- a/tests/prague/eip2537_bls_12_381_precompiles/spec.py +++ b/tests/prague/eip2537_bls_12_381_precompiles/spec.py @@ -2,9 +2,10 @@ from dataclasses import dataclass from enum import Enum, auto -from typing import Callable, Tuple +from typing import Callable, Dict, Tuple from execution_testing import BytesConcatenation +from execution_testing.forks.gas_costs import GasCosts @dataclass(frozen=True) @@ -288,3 +289,41 @@ def pairing_gas(input_length: int) -> int: Spec.MAP_FP_TO_G1: lambda _: Spec.MAP_FP_TO_G1_GAS, Spec.MAP_FP2_TO_G2: lambda _: Spec.MAP_FP2_TO_G2_GAS, } + + +def _pairing_gas_from_costs( + gas_costs: GasCosts, +) -> Callable[[int], int]: + """Build a pairing gas calculator from gas costs.""" + + def calc(input_length: int) -> int: + k = input_length // Spec.LEN_PER_PAIR + return ( + gas_costs.G_PRECOMPILE_BLS_PAIRING_PER_PAIR * k + + gas_costs.G_PRECOMPILE_BLS_PAIRING_BASE + ) + + return calc + + +def build_gas_calculation_function_map( + gas_costs: GasCosts, +) -> Dict[int, Callable[[int], int]]: + """Build a gas calculation function map from fork gas costs.""" + return { + Spec.G1ADD: lambda _: gas_costs.G_PRECOMPILE_BLS_G1ADD, + Spec.G1MSM: msm_gas_func_gen( + BLS12Group.G1, + len(PointG1() + Scalar()), + gas_costs.G_PRECOMPILE_BLS_G1MUL, + ), + Spec.G2ADD: lambda _: gas_costs.G_PRECOMPILE_BLS_G2ADD, + Spec.G2MSM: msm_gas_func_gen( + BLS12Group.G2, + len(PointG2() + Scalar()), + gas_costs.G_PRECOMPILE_BLS_G2MUL, + ), + Spec.PAIRING: _pairing_gas_from_costs(gas_costs), + Spec.MAP_FP_TO_G1: lambda _: gas_costs.G_PRECOMPILE_BLS_G1MAP, + Spec.MAP_FP2_TO_G2: lambda _: gas_costs.G_PRECOMPILE_BLS_G2MAP, + } diff --git a/tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py b/tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py index 1bab28dadc..363e3d99a7 100644 --- a/tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py +++ b/tests/prague/eip2537_bls_12_381_precompiles/test_bls12_pairing.py @@ -5,6 +5,8 @@ [EIP-2537: Precompile for BLS12-381 curve operations](https://eips.ethereum.org/EIPS/eip-2537). """ +from typing import Tuple + import pytest from execution_testing import ( EOA, @@ -140,6 +142,59 @@ def test_valid( ) +def binary_search( + *, + fork: Fork, + max_gas_limit: int, + iteration_data: bytes, + suffix_data: bytes = b"", + extra_gas: int = 100_000, +) -> Tuple[int, bytes]: + """ + Calculate the optimal transaction gas limit and input data to stay below + the maximum gas limit. + """ + intrinsic_gas_cost_calculator = ( + fork.transaction_intrinsic_cost_calculator() + ) + memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() + len_iteration_data = len(iteration_data) + len_prefix_data = len(suffix_data) + + def calc_tx_gas_limit(n: int) -> int: + return ( + extra_gas + + intrinsic_gas_cost_calculator( + calldata=(iteration_data * n) + suffix_data + ) + + memory_expansion_gas_calculator( + new_bytes=(len_iteration_data * n) + len_prefix_data, + ) + + pairing_gas((len_iteration_data * n) + len_prefix_data) + ) + + low = 1 + high = 2 + + while calc_tx_gas_limit(high) < max_gas_limit: + low = high + high *= 2 + + # Binary search for exact fit + while low < high: + mid = (low + high) // 2 + if calc_tx_gas_limit(mid) > max_gas_limit: + high = mid + else: + low = mid + 1 + + best_iterations = low - 1 + return ( + calc_tx_gas_limit(best_iterations), + (iteration_data * best_iterations) + suffix_data, + ) + + @pytest.mark.slow @pytest.mark.parametrize("precompile_gas", [None], ids=[""]) @pytest.mark.parametrize("expected_output", [Spec.PAIRING_TRUE], ids=[""]) @@ -155,40 +210,22 @@ def test_valid_multi_inf( Test maximum input given the current environment gas limit for the BLS12_PAIRING precompile. """ - intrinsic_gas_cost_calculator = ( - fork.transaction_intrinsic_cost_calculator() - ) - memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() gas_costs = fork.gas_costs() extra_gas = 70_000 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD - tx_gas_limit_cap = fork.transaction_gas_limit_cap() - max_gas_limit = ( - Environment().gas_limit - if tx_gas_limit_cap is None - else tx_gas_limit_cap - ) + max_gas_limit = fork.transaction_gas_limit_cap() or Environment().gas_limit inf_data = Spec.INF_G1 + Spec.INF_G2 - input_data = inf_data - while True: - precompile_gas = pairing_gas(len(input_data + inf_data)) - new_tx_gas_limit = ( - extra_gas - + intrinsic_gas_cost_calculator(calldata=input_data + inf_data) - + memory_expansion_gas_calculator( - new_bytes=len(input_data + inf_data) - ) - + precompile_gas - ) - if new_tx_gas_limit > max_gas_limit: - break - tx_gas_limit = new_tx_gas_limit - input_data += inf_data + gas_limit, input_data = binary_search( + fork=fork, + max_gas_limit=max_gas_limit, + iteration_data=inf_data, + extra_gas=extra_gas, + ) tx = Transaction( - gas_limit=tx_gas_limit, + gas_limit=gas_limit, data=input_data, to=call_contract_address, sender=sender, @@ -380,40 +417,24 @@ def test_invalid_multi_inf( Test maximum input given the current environment gas limit for the BLS12_PAIRING precompile and an invalid tail. """ - intrinsic_gas_cost_calculator = ( - fork.transaction_intrinsic_cost_calculator() - ) - memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator() gas_costs = fork.gas_costs() extra_gas = 70_000 + gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD - tx_gas_limit_cap = fork.transaction_gas_limit_cap() - max_gas_limit = ( - Environment().gas_limit - if tx_gas_limit_cap is None - else tx_gas_limit_cap - ) + max_gas_limit = fork.transaction_gas_limit_cap() or Environment().gas_limit inf_data = Spec.INF_G1 + Spec.INF_G2 - input_data = PointG1(Spec.P, 0) + Spec.INF_G2 + invalid_data = PointG1(Spec.P, 0) + Spec.INF_G2 - while True: - precompile_gas = pairing_gas(len(input_data + inf_data)) - new_tx_gas_limit = ( - extra_gas - + intrinsic_gas_cost_calculator(calldata=input_data + inf_data) - + memory_expansion_gas_calculator( - new_bytes=len(input_data + inf_data) - ) - + precompile_gas - ) - if new_tx_gas_limit > max_gas_limit: - break - tx_gas_limit = new_tx_gas_limit - input_data = inf_data + input_data + gas_limit, input_data = binary_search( + fork=fork, + max_gas_limit=max_gas_limit, + iteration_data=inf_data, + suffix_data=invalid_data, + extra_gas=extra_gas, + ) tx = Transaction( - gas_limit=tx_gas_limit, + gas_limit=gas_limit, data=input_data, to=call_contract_address, sender=sender, diff --git a/tests/prague/eip2935_historical_block_hashes_from_state/test_contract_deployment.py b/tests/prague/eip2935_historical_block_hashes_from_state/test_contract_deployment.py index 79b78d5158..980980acb4 100644 --- a/tests/prague/eip2935_historical_block_hashes_from_state/test_contract_deployment.py +++ b/tests/prague/eip2935_historical_block_hashes_from_state/test_contract_deployment.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Dict, Generator -import pytest from execution_testing import ( Account, Address, @@ -25,10 +24,6 @@ REFERENCE_SPEC_VERSION = ref_spec_2935.version -@pytest.mark.pre_alloc_group( - "separate", - reason="Deploys history storage system contract at hardcoded address", -) @generate_system_contract_deploy_test( fork=Prague, tx_json_path=Path(realpath(__file__)).parent / "contract_deploy_tx.json", diff --git a/tests/prague/eip6110_deposits/test_deposits.py b/tests/prague/eip6110_deposits/test_deposits.py index 7f69c23ee9..f0299c2a85 100644 --- a/tests/prague/eip6110_deposits/test_deposits.py +++ b/tests/prague/eip6110_deposits/test_deposits.py @@ -915,10 +915,6 @@ ], ) @pytest.mark.slow() -@pytest.mark.pre_alloc_group( - "deposit_requests", - reason="Tests standard deposit request functionality with system contract", -) def test_deposit( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -1178,10 +1174,6 @@ def test_deposit( ], ) @pytest.mark.exception_test -@pytest.mark.pre_alloc_group( - "deposit_requests", - reason="Tests standard deposit request functionality with system contract", -) def test_deposit_negative( blockchain_test: BlockchainTestFiller, pre: Alloc, diff --git a/tests/prague/eip6110_deposits/test_modified_contract.py b/tests/prague/eip6110_deposits/test_modified_contract.py index 8ab56a6775..be34749ea1 100644 --- a/tests/prague/eip6110_deposits/test_modified_contract.py +++ b/tests/prague/eip6110_deposits/test_modified_contract.py @@ -22,7 +22,7 @@ pytestmark = [ pytest.mark.valid_from("Prague"), - pytest.mark.execute(pytest.mark.skip(reason="modifies pre-alloc")), + pytest.mark.pre_alloc_mutable(), ] REFERENCE_SPEC_GIT_PATH = ref_spec_6110.git_path @@ -73,38 +73,10 @@ @pytest.mark.parametrize( "include_deposit_event,extra_event_type", [ - pytest.param( - True, - "transfer_log", - marks=pytest.mark.pre_alloc_group( - "deposit_extra_logs_with_event_transfer", - reason="Deposit contract with Transfer log AND deposit event", - ), - ), - pytest.param( - True, - "no_topics", - marks=pytest.mark.pre_alloc_group( - "deposit_extra_logs_with_event_no_topics", - reason="Deposit contract with no-topics log AND deposit event", - ), - ), - pytest.param( - False, - "transfer_log", - marks=pytest.mark.pre_alloc_group( - "deposit_extra_logs_no_event_transfer", - reason="Deposit contract with Transfer log NO deposit event", - ), - ), - pytest.param( - False, - "no_topics", - marks=pytest.mark.pre_alloc_group( - "deposit_extra_logs_no_event_no_topics", - reason="Deposit contract with no-topics log NO deposit event", - ), - ), + pytest.param(True, "transfer_log"), + pytest.param(True, "no_topics"), + pytest.param(False, "transfer_log"), + pytest.param(False, "no_topics"), ], ) def test_extra_logs( @@ -205,14 +177,7 @@ def test_extra_logs( @pytest.mark.parametrize( "log_argument,value", [ - pytest.param( - arg, - val, - marks=pytest.mark.pre_alloc_group( - f"deposit_layout_{arg}_{val}", - reason=f"Deposit contract with invalid {arg} set to {val}", - ), - ) + pytest.param(arg, val) for arg in EVENT_ARGUMENTS for val in EVENT_ARGUMENT_VALUES ], @@ -266,25 +231,7 @@ def test_invalid_layout( ) -@pytest.mark.parametrize( - "slice_bytes", - [ - pytest.param( - True, - marks=pytest.mark.pre_alloc_group( - "deposit_log_length_short", - reason="Deposit contract with shortened log data", - ), - ), - pytest.param( - False, - marks=pytest.mark.pre_alloc_group( - "deposit_log_length_long", - reason="Deposit contract with lengthened log data", - ), - ), - ], -) +@pytest.mark.parametrize("slice_bytes", [True, False]) @pytest.mark.exception_test def test_invalid_log_length( blockchain_test: BlockchainTestFiller, pre: Alloc, slice_bytes: bool diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py b/tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py index f7160a6ea6..8978925c6e 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Any, Generator -import pytest from execution_testing import ( Address, Alloc, @@ -25,10 +24,6 @@ REFERENCE_SPEC_VERSION = ref_spec_7002.version -@pytest.mark.pre_alloc_group( - "separate", - reason="Deploys withdrawal system contract at hardcoded predeploy address", -) @generate_system_contract_deploy_test( fork=Prague, tx_json_path=Path(realpath(__file__)).parent / "contract_deploy_tx.json", @@ -49,7 +44,6 @@ def test_system_contract_deployment( fee=Spec.get_fee(0), source_address=sender, ) - pre.fund_address(sender, withdrawal_request.value) intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() test_transaction_gas = intrinsic_gas_calculator( calldata=withdrawal_request.calldata diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py b/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py index 6e351bfd60..adce6a08e0 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_modified_withdrawal_contract.py @@ -29,7 +29,10 @@ REFERENCE_SPEC_GIT_PATH: str = ref_spec_7002.git_path REFERENCE_SPEC_VERSION: str = ref_spec_7002.version -pytestmark: pytest.MarkDecorator = pytest.mark.valid_from("Prague") +pytestmark: List[pytest.MarkDecorator] = [ + pytest.mark.valid_from("Prague"), + pytest.mark.pre_alloc_mutable(), +] def withdrawal_list_with_custom_fee(n: int) -> List[WithdrawalRequest]: # noqa: D103 @@ -82,9 +85,6 @@ def withdrawal_list_with_custom_fee(n: int) -> List[WithdrawalRequest]: # noqa: ), ], ) -@pytest.mark.pre_alloc_group( - "separate", reason="Deploys custom withdrawal contract bytecode" -) def test_extra_withdrawals( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -144,9 +144,6 @@ def test_extra_withdrawals( "system_contract", [Address(Spec_EIP7002.WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS)], ) -@pytest.mark.pre_alloc_group( - "separate", reason="Deploys custom withdrawal contract bytecode" -) @generate_system_contract_error_test( # type: ignore[arg-type] max_gas_limit=Spec_EIP7002.SYSTEM_CALL_GAS_LIMIT, ) diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py index 3f1a2f90a1..6bdeaa5a1f 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests.py @@ -662,10 +662,6 @@ ), ], ) -@pytest.mark.pre_alloc_group( - "withdrawal_requests", - reason="Tests standard withdrawal request functionality", -) def test_withdrawal_requests( blockchain_test: BlockchainTestFiller, blocks: List[Block], @@ -839,10 +835,6 @@ def test_withdrawal_requests( ], ) @pytest.mark.exception_test -@pytest.mark.pre_alloc_group( - "withdrawal_requests", - reason="Tests standard withdrawal request functionality", -) def test_withdrawal_requests_negative( pre: Alloc, fork: Fork, diff --git a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py index 72d72e8ff3..2f8118690e 100644 --- a/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py +++ b/tests/prague/eip7002_el_triggerable_withdrawals/test_withdrawal_requests_during_fork.py @@ -81,9 +81,7 @@ ], ) @pytest.mark.parametrize("timestamp", [15_000 - BLOCKS_BEFORE_FORK], ids=[""]) -@pytest.mark.pre_alloc_group( - "separate", reason="Deploys withdrawal system contract at fork transition" -) +@pytest.mark.pre_alloc_mutable def test_withdrawal_requests_during_fork( blockchain_test: BlockchainTestFiller, blocks: List[Block], diff --git a/tests/prague/eip7251_consolidations/test_consolidations.py b/tests/prague/eip7251_consolidations/test_consolidations.py index 5d4c1fe13a..52eb22c57e 100644 --- a/tests/prague/eip7251_consolidations/test_consolidations.py +++ b/tests/prague/eip7251_consolidations/test_consolidations.py @@ -677,10 +677,6 @@ ), ], ) -@pytest.mark.pre_alloc_group( - "consolidation_requests", - reason="Tests standard consolidation request functionality", -) def test_consolidation_requests( blockchain_test: BlockchainTestFiller, blocks: List[Block], @@ -876,10 +872,6 @@ def test_consolidation_requests( ], ) @pytest.mark.exception_test -@pytest.mark.pre_alloc_group( - "consolidation_requests", - reason="Tests standard consolidation request functionality", -) def test_consolidation_requests_negative( pre: Alloc, fork: Fork, diff --git a/tests/prague/eip7251_consolidations/test_consolidations_during_fork.py b/tests/prague/eip7251_consolidations/test_consolidations_during_fork.py index f8208d4064..3a88358073 100644 --- a/tests/prague/eip7251_consolidations/test_consolidations_during_fork.py +++ b/tests/prague/eip7251_consolidations/test_consolidations_during_fork.py @@ -81,10 +81,7 @@ ], ) @pytest.mark.parametrize("timestamp", [15_000 - BLOCKS_BEFORE_FORK], ids=[""]) -@pytest.mark.pre_alloc_group( - "separate", - reason="Deploys consolidation system contract at fork transition", -) +@pytest.mark.pre_alloc_mutable def test_consolidation_requests_during_fork( blockchain_test: BlockchainTestFiller, blocks: List[Block], diff --git a/tests/prague/eip7251_consolidations/test_contract_deployment.py b/tests/prague/eip7251_consolidations/test_contract_deployment.py index b1e14f8ef9..f0967a8306 100644 --- a/tests/prague/eip7251_consolidations/test_contract_deployment.py +++ b/tests/prague/eip7251_consolidations/test_contract_deployment.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Any, Generator -import pytest from execution_testing import ( Address, Alloc, @@ -25,10 +24,6 @@ REFERENCE_SPEC_VERSION = ref_spec_7251.version -@pytest.mark.pre_alloc_group( - "separate", - reason="Deploys consolidation system contract at hardcoded address", -) @generate_system_contract_deploy_test( fork=Prague, tx_json_path=Path(realpath(__file__)).parent / "contract_deploy_tx.json", @@ -51,7 +46,6 @@ def test_system_contract_deployment( fee=Spec.get_fee(0), source_address=sender, ) - pre.fund_address(sender, consolidation_request.value) intrinsic_gas_calculator = fork.transaction_intrinsic_cost_calculator() test_transaction_gas = intrinsic_gas_calculator( calldata=consolidation_request.calldata diff --git a/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py b/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py index 4e31c166bf..fce8739946 100644 --- a/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py +++ b/tests/prague/eip7251_consolidations/test_modified_consolidation_contract.py @@ -29,7 +29,10 @@ REFERENCE_SPEC_GIT_PATH: str = ref_spec_7251.git_path REFERENCE_SPEC_VERSION: str = ref_spec_7251.version -pytestmark: pytest.MarkDecorator = pytest.mark.valid_from("Prague") +pytestmark: List[pytest.MarkDecorator] = [ + pytest.mark.valid_from("Prague"), + pytest.mark.pre_alloc_mutable(), +] def consolidation_list_with_custom_fee(n: int) -> List[ConsolidationRequest]: # noqa: D103 @@ -82,9 +85,6 @@ def consolidation_list_with_custom_fee(n: int) -> List[ConsolidationRequest]: # ), ], ) -@pytest.mark.pre_alloc_group( - "separate", reason="Deploys custom consolidation contract bytecode" -) def test_extra_consolidations( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -145,9 +145,6 @@ def test_extra_consolidations( "system_contract", [Address(Spec_EIP7251.CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS)], ) -@pytest.mark.pre_alloc_group( - "separate", reason="Deploys custom consolidation contract bytecode" -) @generate_system_contract_error_test( # type: ignore[arg-type] max_gas_limit=Spec_EIP7251.SYSTEM_CALL_GAS_LIMIT, ) diff --git a/tests/prague/eip7623_increase_calldata_cost/test_refunds.py b/tests/prague/eip7623_increase_calldata_cost/test_refunds.py index 2926e1ae8c..9eee2a80a3 100644 --- a/tests/prague/eip7623_increase_calldata_cost/test_refunds.py +++ b/tests/prague/eip7623_increase_calldata_cost/test_refunds.py @@ -111,12 +111,14 @@ def prefix_code_gas(fork: Fork, refund_type: RefundType) -> int: """Return the minimum execution gas cost due to the refund type.""" if RefundType.STORAGE_CLEAR in refund_type: # Minimum code to generate a storage clear is Op.SSTORE(0, 0). - gas_costs = fork.gas_costs() return ( - gas_costs.G_COLD_SLOAD - + gas_costs.G_STORAGE_RESET - + (gas_costs.G_VERY_LOW * 2) - ) + Op.SSTORE( + key_warm=False, + original_value=1, + new_value=0, + ) + + Op.PUSH1(0) * 2 + ).gas_cost(fork) return 0 diff --git a/tests/prague/eip7685_general_purpose_el_requests/test_multi_type_requests.py b/tests/prague/eip7685_general_purpose_el_requests/test_multi_type_requests.py index 2a164790b9..c6bc3bc17b 100644 --- a/tests/prague/eip7685_general_purpose_el_requests/test_multi_type_requests.py +++ b/tests/prague/eip7685_general_purpose_el_requests/test_multi_type_requests.py @@ -336,10 +336,6 @@ def get_contract_permutations( ), ], ) -@pytest.mark.pre_alloc_group( - "multi_type_requests", - reason="Tests combinations of multiple request types", -) def test_valid_multi_type_requests( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -358,10 +354,6 @@ def test_valid_multi_type_requests( @pytest.mark.parametrize("requests", [*get_permutations()]) -@pytest.mark.pre_alloc_group( - "multi_type_requests", - reason="Tests combinations of multiple request types", -) def test_valid_multi_type_request_from_same_tx( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -672,10 +664,6 @@ def func(fork: Fork) -> List[ParameterSet]: invalid_requests_block_combinations(correct_requests_hash_in_header=False), ) @pytest.mark.exception_test -@pytest.mark.pre_alloc_group( - "multi_type_requests", - reason="Tests combinations of multiple request types", -) def test_invalid_multi_type_requests( blockchain_test: BlockchainTestFiller, pre: Alloc, @@ -705,10 +693,6 @@ def test_invalid_multi_type_requests( @pytest.mark.parametrize("correct_requests_hash_in_header", [True]) @pytest.mark.blockchain_test_engine_only @pytest.mark.exception_test -@pytest.mark.pre_alloc_group( - "multi_type_requests", - reason="Tests combinations of multiple request types", -) def test_invalid_multi_type_requests_engine( blockchain_test: BlockchainTestFiller, pre: Alloc, diff --git a/tests/prague/eip7702_set_code_tx/test_eip_mainnet.py b/tests/prague/eip7702_set_code_tx/test_eip_mainnet.py index 6470700815..8702fd1514 100644 --- a/tests/prague/eip7702_set_code_tx/test_eip_mainnet.py +++ b/tests/prague/eip7702_set_code_tx/test_eip_mainnet.py @@ -50,17 +50,18 @@ def test_eip_7702( signer=auth_signer, ), ] - gas_costs = fork.gas_costs() intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator() intrinsic_gas_cost = intrinsic_gas_cost_calc( access_list=[], authorization_list_or_count=authorization_list, ) execution_cost = ( - (gas_costs.G_COLD_SLOAD + gas_costs.G_STORAGE_SET) * 3 - + (gas_costs.G_VERY_LOW * 3) - + (gas_costs.G_BASE * 3) - ) + Op.SSTORE(key_warm=False) * 3 + + Op.PUSH1(0) * 3 + + Op.ORIGIN + + Op.CALLER + + Op.CALLVALUE + ).gas_cost(fork) tx = Transaction( gas_limit=intrinsic_gas_cost + execution_cost, diff --git a/tests/prague/eip7702_set_code_tx/test_gas.py b/tests/prague/eip7702_set_code_tx/test_gas.py index 06c39b6e0e..ab3d162fe4 100644 --- a/tests/prague/eip7702_set_code_tx/test_gas.py +++ b/tests/prague/eip7702_set_code_tx/test_gas.py @@ -22,7 +22,6 @@ BalNonceChange, BlockAccessListExpectation, Bytecode, - Bytes, ChainConfig, CodeGasMeasure, Fork, @@ -181,10 +180,7 @@ def generator( assert not self_sponsored or i > 0, ( "Self-sponsored contract authority is not supported" ) - authority = pre.fund_eoa() - authority_account = pre[authority] - assert authority_account is not None - authority_account.code = Bytes(Op.STOP) + authority = pre.fund_eoa(code=Op.STOP) yield AuthorityWithProperties( authority=authority, address_type=current_authority_type, @@ -726,7 +722,7 @@ def gas_test_parameter_args( { "authority_type": AddressType.CONTRACT, }, - marks=pytest.mark.pre_alloc_modify, + marks=[pytest.mark.pre_alloc_mutable], id="single_valid_authorization_invalid_contract_authority", ), pytest.param( @@ -738,7 +734,7 @@ def gas_test_parameter_args( ], "authorizations_count": multiple_authorizations_count, }, - marks=pytest.mark.pre_alloc_modify, + marks=[pytest.mark.pre_alloc_mutable], id="multiple_authorizations_empty_account_then_contract_authority", ), pytest.param( @@ -747,7 +743,7 @@ def gas_test_parameter_args( "authority_type": [AddressType.EOA, AddressType.CONTRACT], "authorizations_count": multiple_authorizations_count, }, - marks=pytest.mark.pre_alloc_modify, + marks=[pytest.mark.pre_alloc_mutable], id="multiple_authorizations_eoa_then_contract_authority", ), pytest.param( @@ -757,7 +753,7 @@ def gas_test_parameter_args( "authority_type": [AddressType.EOA, AddressType.CONTRACT], "authorizations_count": multiple_authorizations_count, }, - marks=pytest.mark.pre_alloc_modify, + marks=[pytest.mark.pre_alloc_mutable], id="multiple_authorizations_eoa_self_sponsored_then_contract_authority", ), ] @@ -887,20 +883,14 @@ def test_gas_cost( # SSTORE opcodes in order to make sure that the refund is less than one # fifth (EIP-3529) of the total gas used, so we can see the full discount # being reflected in most of the tests. - gas_costs = fork.gas_costs() - gas_opcode_cost = gas_costs.G_BASE + gas_opcode_cost = Op.GAS.gas_cost(fork) sstore_opcode_count = 10 push_opcode_count = (2 * (sstore_opcode_count)) - 1 - push_opcode_cost = gas_costs.G_VERY_LOW * push_opcode_count - sstore_opcode_cost = gas_costs.G_STORAGE_SET * sstore_opcode_count - cold_storage_cost = gas_costs.G_COLD_SLOAD * sstore_opcode_count - execution_gas = ( - gas_opcode_cost - + push_opcode_cost - + sstore_opcode_cost - + cold_storage_cost - ) + Op.GAS + + Op.PUSH1(0) * push_opcode_count + + Op.SSTORE(key_warm=False) * sstore_opcode_count + ).gas_cost(fork) # The first opcode that executes in the code is the GAS opcode, which costs # 2 gas, so we subtract that from the expected gas measure. @@ -1256,21 +1246,20 @@ def test_call_to_pre_authorized_oog( # Callee tries to call the auth_signer which delegates # to the delegation contract. The call instruction should out-of-gas # because of the addition cost of the delegation account access. - callee_code = Bytecode( - Op.SSTORE(0, call_opcode(gas=0, address=auth_signer)), - ) + callee_code = Op.SSTORE(0, call_opcode(gas=0, address=auth_signer)) callee_storage = Storage() callee_storage[0] = 0xFF # Value other than 0 or 1. Should not be changed. callee_address = pre.deploy_contract(callee_code, storage=callee_storage) - gas_costs = fork.gas_costs() intrinsic_gas_cost_calculator = ( fork.transaction_intrinsic_cost_calculator() ) tx_gas_limit = ( intrinsic_gas_cost_calculator() - + len(call_opcode.kwargs) * gas_costs.G_VERY_LOW - + (gas_costs.G_COLD_ACCOUNT_ACCESS * 2) + + ( + Op.PUSH1(0) * len(call_opcode.kwargs) + + call_opcode(address_warm=False, delegated_address=True) + ).gas_cost(fork) - 1 ) tx = Transaction( 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 2b369f9909..a0639a8fe7 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 @@ -54,13 +54,7 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_7702.git_path REFERENCE_SPEC_VERSION = ref_spec_7702.version -pytestmark = [ - pytest.mark.valid_from("Prague"), - pytest.mark.pre_alloc_group( - "set_code_tests", - reason="Tests EIP-7702 set code transactions with system contracts", - ), -] +pytestmark = pytest.mark.valid_from("Prague") auth_account_start_balance = 0 @@ -706,12 +700,9 @@ def test_delegated_eoa_can_send_creating_tx( ) assert initcode_len == len(initcode) - gas_costs = fork.gas_costs() - tx = Transaction( ty=tx_type, - gas_limit=200_000 - + (gas_costs.G_STORAGE_SET + gas_costs.G_COLD_SLOAD) * 7, + gas_limit=200_000 + (Op.SSTORE(key_warm=False) * 7).gas_cost(fork), to=None, value=0, data=initcode, @@ -2671,17 +2662,11 @@ def test_valid_tx_invalid_chain_id( Spec.MAX_NONCE, Spec.MAX_NONCE, id="nonce=2**64-1", - marks=pytest.mark.execute( - pytest.mark.skip(reason="Impossible account nonce") - ), ), pytest.param( Spec.MAX_NONCE - 1, Spec.MAX_NONCE - 1, id="nonce=2**64-2", - marks=pytest.mark.execute( - pytest.mark.skip(reason="Impossible account nonce") - ), ), pytest.param( 0, @@ -2695,7 +2680,7 @@ def test_valid_tx_invalid_chain_id( ), ], ) -@pytest.mark.execute(pytest.mark.skip(reason="Non-zero nonce not supported")) +@pytest.mark.pre_alloc_mutable() def test_nonce_validity( state_test: StateTestFiller, pre: Alloc, @@ -2767,7 +2752,7 @@ def test_nonce_validity( ) -@pytest.mark.execute(pytest.mark.skip(reason="Impossible account nonce")) +@pytest.mark.pre_alloc_mutable() def test_nonce_overflow_after_first_authorization( state_test: StateTestFiller, pre: Alloc, @@ -3153,8 +3138,6 @@ def test_set_code_to_system_contract( ) caller_code_address = pre.deploy_contract(caller_code) sender = pre.fund_eoa() - if call_value > 0: - pre.fund_address(sender, call_value) txs = [ Transaction( @@ -4055,9 +4038,7 @@ def test_authorization_reusing_nonce( [True, False], ) @pytest.mark.exception_test -@pytest.mark.execute( - pytest.mark.skip(reason="Requires contract-eoa address collision") -) +@pytest.mark.pre_alloc_mutable def test_set_code_from_account_with_non_delegating_code( state_test: StateTestFiller, pre: Alloc, @@ -4072,7 +4053,9 @@ def test_set_code_from_account_with_non_delegating_code( delegating) But at the same time it has auth tuple that will point this sender account To be eoa, delegation, contract .. etc """ - sender = pre.fund_eoa(nonce=1) + # Set the sender account to have some code, that is specifically not a + # delegation. + sender = pre.fund_eoa(nonce=1, code=Op.STOP) random_address = pre.fund_eoa(0) set_code_to_address: Address @@ -4090,12 +4073,6 @@ def test_set_code_from_account_with_non_delegating_code( raise ValueError(f"Unsupported set code type: {set_code_type}") callee_address = pre.deploy_contract(Op.SSTORE(0, 1) + Op.STOP) - # Set the sender account to have some code, that is specifically not a - # delegation. - sender_account = pre[sender] - assert sender_account is not None - sender_account.code = Bytes(Op.STOP) - tx = Transaction( gas_limit=100_000, to=callee_address, diff --git a/tests/prague/eip7702_set_code_tx/test_set_code_txs_2.py b/tests/prague/eip7702_set_code_tx/test_set_code_txs_2.py index 1a84fdb946..ae3a34ca62 100644 --- a/tests/prague/eip7702_set_code_tx/test_set_code_txs_2.py +++ b/tests/prague/eip7702_set_code_tx/test_set_code_txs_2.py @@ -18,7 +18,6 @@ Conditional, Environment, Fork, - GasCosts, Hash, Macros, Op, @@ -698,85 +697,60 @@ def test_gas_diff_pointer_vs_direct_call( sender = pre.fund_eoa() pointer_a = pre.fund_eoa() call_worked = 1 - gas_costs: GasCosts = fork.gas_costs() - + # The contract code is: + # Op.SSTORE(call_worked, Op.ADD(Op.SLOAD(call_worked), 1)) + # The opcodes_price captures the remaining push/add overhead. opcodes_price = 37 + + direct_account_warm = access_list_rule in [ + AccessListCall.IN_NORMAL_TX_ONLY, + AccessListCall.IN_BOTH_TX, + ] + direct_storage_warm = direct_account_warm direct_call_gas: int = ( - # 20_000 + 2_600 + 2_100 + 37 = 24737 - gas_costs.G_STORAGE_SET - + ( - # access account price - # If storage and account is declared in access list then discount - gas_costs.G_WARM_ACCOUNT_ACCESS + gas_costs.G_WARM_SLOAD - if access_list_rule - in [AccessListCall.IN_NORMAL_TX_ONLY, AccessListCall.IN_BOTH_TX] - else gas_costs.G_COLD_ACCOUNT_ACCESS + gas_costs.G_COLD_SLOAD - ) + Op.SSTORE(key_warm=True).gas_cost(fork) # key warmed by prior SLOAD + + Op.CALL(address_warm=direct_account_warm).gas_cost(fork) + + Op.SLOAD(key_warm=direct_storage_warm).gas_cost(fork) + opcodes_price ) + pointer_account_warm = ( + pointer_definition + in [ + PointerDefinition.IN_BOTH_TX, + PointerDefinition.IN_POINTER_TX_ONLY, + ] + or access_list_rule + in [ + AccessListCall.IN_BOTH_TX, + AccessListCall.IN_POINTER_TX_ONLY, + ] + and access_list_to == AccessListTo.POINTER_ADDRESS + ) + pointer_storage_warm = ( + access_list_rule + in [ + AccessListCall.IN_BOTH_TX, + AccessListCall.IN_POINTER_TX_ONLY, + ] + and access_list_to == AccessListTo.POINTER_ADDRESS + ) + pointer_contract_warm = ( + access_list_rule + in [ + AccessListCall.IN_BOTH_TX, + AccessListCall.IN_POINTER_TX_ONLY, + ] + and access_list_to == AccessListTo.CONTRACT_ADDRESS + ) pointer_call_gas: int = ( - # sstore + addr + addr + sload + op - # no access list, no pointer, all accesses are hot - # 20_000 + 2_600 * 2 + 2_100 + 37 = 27_337 - # - # access list for pointer, pointer is set - # additional 2_600 charged for access of contract - # 20_000 + 100 + 2_600 + 100 + 37 = 22_837 - # - # no access list, pointer is set - # pointer access is hot, sload and contract are hot - # 20_000 + 100 + 2_600 + 2_100 + 37 = 24_837 - # - # access list for contract, pointer is set - # contract call is hot, pointer call is call because pointer is set - # only sload is hot because access list is for contract - # 20_000 + 100 + 100 + 2100 + 37 = 22_337 - gas_costs.G_STORAGE_SET + Op.SSTORE(key_warm=True).gas_cost(fork) # key warmed by prior SLOAD # pointer address access - + ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if ( - pointer_definition - in [ - PointerDefinition.IN_BOTH_TX, - PointerDefinition.IN_POINTER_TX_ONLY, - ] - or access_list_rule - in [ - AccessListCall.IN_BOTH_TX, - AccessListCall.IN_POINTER_TX_ONLY, - ] - and access_list_to == AccessListTo.POINTER_ADDRESS - ) - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) + + Op.CALL(address_warm=pointer_account_warm).gas_cost(fork) # storage access - + ( - gas_costs.G_WARM_SLOAD - if ( - access_list_rule - in [ - AccessListCall.IN_BOTH_TX, - AccessListCall.IN_POINTER_TX_ONLY, - ] - and access_list_to == AccessListTo.POINTER_ADDRESS - ) - else gas_costs.G_COLD_SLOAD - ) + + Op.SLOAD(key_warm=pointer_storage_warm).gas_cost(fork) # contract address access - + ( - gas_costs.G_WARM_ACCOUNT_ACCESS - if ( - access_list_rule - in [ - AccessListCall.IN_BOTH_TX, - AccessListCall.IN_POINTER_TX_ONLY, - ] - and access_list_to == AccessListTo.CONTRACT_ADDRESS - ) - else gas_costs.G_COLD_ACCOUNT_ACCESS - ) + + Op.CALL(address_warm=pointer_contract_warm).gas_cost(fork) + opcodes_price ) @@ -919,22 +893,19 @@ def test_pointer_call_followed_by_direct_call( sender = pre.fund_eoa() pointer_a = pre.fund_eoa() - gas_costs: GasCosts = fork.gas_costs() call_worked = 1 opcodes_price: int = 37 pointer_call_gas = ( - gas_costs.G_STORAGE_SET - + gas_costs.G_WARM_ACCOUNT_ACCESS # pointer is warm - + gas_costs.G_COLD_ACCOUNT_ACCESS # contract is cold - + gas_costs.G_COLD_SLOAD # storage access under pointer call is cold + Op.SSTORE(key_warm=True).gas_cost(fork) # key warmed by prior SLOAD + + Op.CALL(address_warm=True).gas_cost(fork) # pointer is warm + + Op.CALL(address_warm=False).gas_cost(fork) # contract is cold + + Op.SLOAD(key_warm=False).gas_cost(fork) # storage is cold + opcodes_price ) direct_call_gas = ( - gas_costs.G_STORAGE_SET - + gas_costs.G_WARM_ACCOUNT_ACCESS # since previous pointer call, - # contract is now warm - + gas_costs.G_COLD_SLOAD # but storage is cold, because it's - # contract's direct + Op.SSTORE(key_warm=True).gas_cost(fork) # key warmed by prior SLOAD + + Op.CALL(address_warm=True).gas_cost(fork) # contract is now warm + + Op.SLOAD(key_warm=False).gas_cost(fork) # storage is cold + opcodes_price ) @@ -2155,11 +2126,9 @@ def test_delegation_replacement_call_previous_contract( auth_signer = pre.fund_eoa(delegation=pre_set_delegation_address) sender = pre.fund_eoa() - gsc = fork.gas_costs() - overhead_cost = gsc.G_VERY_LOW * len(Op.CALL.kwargs) + call_code = Op.CALL(gas=0, address=pre_set_delegation_address) set_code = CodeGasMeasure( - code=Op.CALL(gas=0, address=pre_set_delegation_address), - overhead_cost=overhead_cost, + code=call_code, extra_stack_items=1, ) @@ -2187,7 +2156,7 @@ def test_delegation_replacement_call_previous_contract( tx=tx, post={ auth_signer: Account( - storage={0: gsc.G_COLD_ACCOUNT_ACCESS}, + storage={0: call_code.gas_cost(fork)}, ) }, ) diff --git a/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py b/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py index 2857a2bc28..a9c2a18280 100644 --- a/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py +++ b/tests/shanghai/eip3651_warm_coinbase/test_warm_coinbase.py @@ -25,10 +25,6 @@ REFERENCE_SPEC_GIT_PATH = ref_spec_3651.git_path REFERENCE_SPEC_VERSION = ref_spec_3651.version -# Amount of gas required to make a call to a warm account. -# Calling a cold account with this amount of gas results in exception. -GAS_REQUIRED_CALL_WARM_ACCOUNT = 100 - @pytest.mark.valid_from("Shanghai") @pytest.mark.parametrize( @@ -37,32 +33,12 @@ ids=["sufficient_gas", "insufficient_gas"], ) @pytest.mark.parametrize( - "opcode,contract_under_test_code,call_gas_exact", + "opcode,call_opcode", [ - ( - "call", - Op.POP(Op.CALL(0, Op.COINBASE, 0, 0, 0, 0, 0)), - # Extra gas: COINBASE + 4*PUSH1 + 2*DUP1 + POP - GAS_REQUIRED_CALL_WARM_ACCOUNT + 22, - ), - ( - "callcode", - Op.POP(Op.CALLCODE(0, Op.COINBASE, 0, 0, 0, 0, 0)), - # Extra gas: COINBASE + 4*PUSH1 + 2*DUP1 + POP - GAS_REQUIRED_CALL_WARM_ACCOUNT + 22, - ), - ( - "delegatecall", - Op.POP(Op.DELEGATECALL(0, Op.COINBASE, 0, 0, 0, 0)), - # Extra: COINBASE + 3*PUSH1 + 2*DUP1 + POP - GAS_REQUIRED_CALL_WARM_ACCOUNT + 19, - ), - ( - "staticcall", - Op.POP(Op.STATICCALL(0, Op.COINBASE, 0, 0, 0, 0)), - # Extra: COINBASE + 3*PUSH1 + 2*DUP1 + POP - GAS_REQUIRED_CALL_WARM_ACCOUNT + 19, - ), + ("call", Op.CALL), + ("callcode", Op.CALLCODE), + ("delegatecall", Op.DELEGATECALL), + ("staticcall", Op.STATICCALL), ], ids=["CALL", "CALLCODE", "DELEGATECALL", "STATICCALL"], ) @@ -74,8 +50,7 @@ def test_warm_coinbase_call_out_of_gas( sender: Address, fork: Fork, opcode: str, - contract_under_test_code: Bytecode, - call_gas_exact: int, + call_opcode: Op, use_sufficient_gas: bool, ) -> None: """ @@ -87,8 +62,26 @@ def test_warm_coinbase_call_out_of_gas( - DELEGATECALL - STATICCALL """ + # Build contract code: POP(xCALL(0, COINBASE, 0, ...)) + if call_opcode in (Op.CALL, Op.CALLCODE): + contract_under_test_code = Op.POP( + call_opcode(0, Op.COINBASE, 0, 0, 0, 0, 0) + ) + else: + contract_under_test_code = Op.POP( + call_opcode(0, Op.COINBASE, 0, 0, 0, 0) + ) + contract_under_test_address = pre.deploy_contract(contract_under_test_code) + # Compute exact gas: warm call cost + overhead + # (COINBASE, PUSHes, DUPs, POP) + warm_call_cost = call_opcode(address_warm=True).gas_cost(fork) + # Overhead = total cost (with cold default) minus the cold call cost + cold_call_cost = call_opcode(address_warm=False).gas_cost(fork) + total_with_cold = contract_under_test_code.gas_cost(fork) + call_gas_exact = warm_call_cost + (total_with_cold - cold_call_cost) + if not use_sufficient_gas: call_gas_exact -= 1 @@ -130,77 +123,23 @@ def test_warm_coinbase_call_out_of_gas( ) -# List of opcodes that are affected by EIP-3651 +# List of opcodes that are affected by EIP-3651, with their code and +# extra_stack_items. Overhead cost is computed at test time via gas_cost(fork). gas_measured_opcodes = [ - ( - "EXTCODESIZE", - CodeGasMeasure( - code=Op.EXTCODESIZE(Op.COINBASE), - overhead_cost=2, - extra_stack_items=1, - ), - ), - ( - "EXTCODECOPY", - CodeGasMeasure( - code=Op.EXTCODECOPY(Op.COINBASE, 0, 0, 0), - overhead_cost=2 + 3 + 3 + 3, - ), - ), - ( - "EXTCODEHASH", - CodeGasMeasure( - code=Op.EXTCODEHASH(Op.COINBASE), - overhead_cost=2, - extra_stack_items=1, - ), - ), - ( - "BALANCE", - CodeGasMeasure( - code=Op.BALANCE(Op.COINBASE), - overhead_cost=2, - extra_stack_items=1, - ), - ), - ( - "CALL", - CodeGasMeasure( - code=Op.CALL(0xFF, Op.COINBASE, 0, 0, 0, 0, 0), - overhead_cost=3 + 2 + 3 + 3 + 3 + 3 + 3, - extra_stack_items=1, - ), - ), - ( - "CALLCODE", - CodeGasMeasure( - code=Op.CALLCODE(0xFF, Op.COINBASE, 0, 0, 0, 0, 0), - overhead_cost=3 + 2 + 3 + 3 + 3 + 3 + 3, - extra_stack_items=1, - ), - ), - ( - "DELEGATECALL", - CodeGasMeasure( - code=Op.DELEGATECALL(0xFF, Op.COINBASE, 0, 0, 0, 0), - overhead_cost=3 + 2 + 3 + 3 + 3 + 3, - extra_stack_items=1, - ), - ), - ( - "STATICCALL", - CodeGasMeasure( - code=Op.STATICCALL(0xFF, Op.COINBASE, 0, 0, 0, 0), - overhead_cost=3 + 2 + 3 + 3 + 3 + 3, - extra_stack_items=1, - ), - ), + ("EXTCODESIZE", Op.EXTCODESIZE(Op.COINBASE), 1), + ("EXTCODECOPY", Op.EXTCODECOPY(Op.COINBASE, 0, 0, 0), 0), + ("EXTCODEHASH", Op.EXTCODEHASH(Op.COINBASE), 1), + ("BALANCE", Op.BALANCE(Op.COINBASE), 1), + ("CALL", Op.CALL(0xFF, Op.COINBASE, 0, 0, 0, 0, 0), 1), + ("CALLCODE", Op.CALLCODE(0xFF, Op.COINBASE, 0, 0, 0, 0, 0), 1), + ("DELEGATECALL", Op.DELEGATECALL(0xFF, Op.COINBASE, 0, 0, 0, 0), 1), + ("STATICCALL", Op.STATICCALL(0xFF, Op.COINBASE, 0, 0, 0, 0), 1), ] @pytest.mark.valid_from("Berlin") # these tests fill for fork >= Berlin @pytest.mark.parametrize( - "opcode,code_gas_measure", + "opcode,measured_code,extra_stack_items", gas_measured_opcodes, ids=[i[0] for i in gas_measured_opcodes], ) @@ -211,7 +150,8 @@ def test_warm_coinbase_gas_usage( sender: Address, fork: Fork, opcode: str, - code_gas_measure: Bytecode, + measured_code: Bytecode, + extra_stack_items: int, ) -> None: """ Test the gas usage of opcodes affected by assuming a warm coinbase. @@ -225,14 +165,26 @@ def test_warm_coinbase_gas_usage( - DELEGATECALL - STATICCALL """ + # Compute overhead cost: total bytecode cost minus the + # opcode-under-test cost + # The opcode-under-test cost (warm or cold) is what we're measuring + total_code_cost = measured_code.gas_cost(fork) + # The opcode cost with default (cold) metadata + opcode_cold_cost = Op.BALANCE(address_warm=False).gas_cost(fork) + overhead_cost = total_code_cost - opcode_cold_cost + + code_gas_measure = CodeGasMeasure( + code=measured_code, + overhead_cost=overhead_cost, + extra_stack_items=extra_stack_items, + ) + measure_address = pre.deploy_contract( code=code_gas_measure, ) - if fork >= Shanghai: # Warm account access cost after EIP-3651 - expected_gas = GAS_REQUIRED_CALL_WARM_ACCOUNT - else: - expected_gas = 2600 # Cold account access cost before EIP-3651 + # Coinbase is warm after EIP-3651 (Shanghai+), cold before + expected_gas = Op.BALANCE(address_warm=(fork >= Shanghai)).gas_cost(fork) tx = Transaction( to=measure_address, diff --git a/tests/shanghai/eip3860_initcode/test_initcode.py b/tests/shanghai/eip3860_initcode/test_initcode.py index cb3ab420d8..7c6da56564 100644 --- a/tests/shanghai/eip3860_initcode/test_initcode.py +++ b/tests/shanghai/eip3860_initcode/test_initcode.py @@ -31,9 +31,8 @@ from .helpers import ( INITCODE_RESULTING_DEPLOYED_CODE, get_create_id, - get_initcode_name, ) -from .spec import Spec, ref_spec_3860 +from .spec import ref_spec_3860 REFERENCE_SPEC_GIT_PATH = ref_spec_3860.git_path REFERENCE_SPEC_VERSION = ref_spec_3860.version @@ -41,94 +40,101 @@ pytestmark = pytest.mark.valid_from("Shanghai") -"""Initcode templates used throughout the tests""" -INITCODE_ONES_MAX_LIMIT = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=Spec.MAX_INITCODE_SIZE, - padding_byte=0x01, - name="max_size_ones", -) - -INITCODE_ZEROS_MAX_LIMIT = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=Spec.MAX_INITCODE_SIZE, - padding_byte=0x00, - name="max_size_zeros", -) - -INITCODE_ONES_OVER_LIMIT = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=Spec.MAX_INITCODE_SIZE + 1, - padding_byte=0x01, - name="over_limit_ones", -) - -INITCODE_ZEROS_OVER_LIMIT = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=Spec.MAX_INITCODE_SIZE + 1, - padding_byte=0x00, - name="over_limit_zeros", -) - -INITCODE_ZEROS_32_BYTES = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=32, - padding_byte=0x00, - name="32_bytes", -) - -INITCODE_ZEROS_33_BYTES = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=33, - padding_byte=0x00, - name="33_bytes", -) - -INITCODE_ZEROS_49120_BYTES = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=49120, - padding_byte=0x00, - name="49120_bytes", -) - -INITCODE_ZEROS_49121_BYTES = Initcode( - deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, - initcode_length=49121, - padding_byte=0x00, - name="49121_bytes", -) - -EMPTY_INITCODE = Initcode( - name="empty", -) -EMPTY_INITCODE._bytes_ = bytes() -EMPTY_INITCODE.deployment_gas = 0 -EMPTY_INITCODE.execution_gas = 0 +@pytest.fixture +def initcode(fork: Fork, initcode_name: str) -> Initcode: + """Create an Initcode object with fork-specific gas calculations.""" + if initcode_name == "max_size_ones": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=fork.max_initcode_size(), + padding_byte=0x01, + ) + elif initcode_name == "max_size_zeros": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=fork.max_initcode_size(), + padding_byte=0x00, + ) + elif initcode_name == "over_limit_ones": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=fork.max_initcode_size() + 1, + padding_byte=0x01, + ) + elif initcode_name == "over_limit_zeros": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=fork.max_initcode_size() + 1, + padding_byte=0x00, + ) + elif initcode_name == "32_bytes": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=32, + padding_byte=0x00, + ) + elif initcode_name == "33_bytes": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=33, + padding_byte=0x00, + ) + elif initcode_name == "max_size_minus_word": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=fork.max_initcode_size() - 32, + padding_byte=0x00, + ) + elif initcode_name == "max_size_minus_word_plus_byte": + return Initcode( + name=initcode_name, + fork=fork, + deploy_code=INITCODE_RESULTING_DEPLOYED_CODE, + initcode_length=fork.max_initcode_size() - 32 + 1, + padding_byte=0x00, + ) + elif initcode_name == "empty": + ic = Initcode(name=initcode_name, fork=fork) + ic._bytes_ = bytes() + ic.deployment_gas = 0 + ic.execution_gas = 0 + return ic + elif initcode_name == "single_byte": + ic = Initcode(name=initcode_name, fork=fork) + ic._bytes_ = bytes(Op.STOP) + ic.deployment_gas = 0 + ic.execution_gas = 0 + return ic + else: + raise ValueError(f"Unknown initcode_name: {initcode_name}") -SINGLE_BYTE_INITCODE = Initcode( - name="single_byte", -) -SINGLE_BYTE_INITCODE._bytes_ = bytes(Op.STOP) -SINGLE_BYTE_INITCODE.deployment_gas = 0 -SINGLE_BYTE_INITCODE.execution_gas = 0 """Test cases using a contract creating transaction""" @pytest.mark.xdist_group(name="bigmem") @pytest.mark.parametrize( - "initcode", + "initcode_name", [ - INITCODE_ZEROS_MAX_LIMIT, - INITCODE_ONES_MAX_LIMIT, - pytest.param( - INITCODE_ZEROS_OVER_LIMIT, marks=pytest.mark.exception_test - ), - pytest.param( - INITCODE_ONES_OVER_LIMIT, marks=pytest.mark.exception_test - ), + pytest.param("max_size_zeros"), + pytest.param("max_size_ones"), + pytest.param("over_limit_zeros", marks=pytest.mark.exception_test), + pytest.param("over_limit_ones", marks=pytest.mark.exception_test), ], - ids=get_initcode_name, ) def test_contract_creating_tx( state_test: StateTestFiller, @@ -137,6 +143,7 @@ def test_contract_creating_tx( post: Alloc, sender: EOA, initcode: Initcode, + fork: Fork, ) -> None: """ Test creating a contract with initcode that is on/over the allowed limit. @@ -157,7 +164,7 @@ def test_contract_creating_tx( sender=sender, ) - if len(initcode) > Spec.MAX_INITCODE_SIZE: + if len(initcode) > fork.max_initcode_size(): # Initcode is above the max size, tx inclusion in the block makes # it invalid. post[create_contract_address] = Account.NONEXISTENT @@ -175,15 +182,21 @@ def test_contract_creating_tx( ) -def valid_gas_test_case(initcode: Initcode, gas_test_case: str) -> bool: - """Filter out invalid gas test case/initcode combinations.""" - if gas_test_case == "too_little_execution_gas": - return (initcode.deployment_gas + initcode.execution_gas) > 0 +ZERO_GAS_SPECS = {"empty", "single_byte"} + + +def valid_gas_test_case(initcode_name: str, gas_case: str) -> bool: + """Filter invalid gas test case combinations.""" + if ( + gas_case == "too_little_execution_gas" + and initcode_name in ZERO_GAS_SPECS + ): + return False return True @pytest.mark.parametrize( - "initcode,gas_test_case", + "initcode_name,gas_test_case", [ pytest.param( i, @@ -195,14 +208,14 @@ def valid_gas_test_case(initcode: Initcode, gas_test_case: str) -> bool: ), ) for i in [ - INITCODE_ZEROS_MAX_LIMIT, - INITCODE_ONES_MAX_LIMIT, - EMPTY_INITCODE, - SINGLE_BYTE_INITCODE, - INITCODE_ZEROS_32_BYTES, - INITCODE_ZEROS_33_BYTES, - INITCODE_ZEROS_49120_BYTES, - INITCODE_ZEROS_49121_BYTES, + "max_size_zeros", + "max_size_ones", + "empty", + "single_byte", + "32_bytes", + "33_bytes", + "max_size_minus_word", + "max_size_minus_word_plus_byte", ] for g in [ "too_little_intrinsic_gas", @@ -212,9 +225,6 @@ def valid_gas_test_case(initcode: Initcode, gas_test_case: str) -> bool: ] if valid_gas_test_case(i, g) ], - ids=lambda x: ( - f"{get_initcode_name(x[0])}-{x[1]}" if isinstance(x, tuple) else x - ), ) class TestContractCreationGasUsage: """ @@ -397,20 +407,19 @@ def test_gas_usage( @pytest.mark.parametrize( - "initcode", + "initcode_name", [ - INITCODE_ZEROS_MAX_LIMIT, - INITCODE_ONES_MAX_LIMIT, - INITCODE_ZEROS_OVER_LIMIT, - INITCODE_ONES_OVER_LIMIT, - EMPTY_INITCODE, - SINGLE_BYTE_INITCODE, - INITCODE_ZEROS_32_BYTES, - INITCODE_ZEROS_33_BYTES, - INITCODE_ZEROS_49120_BYTES, - INITCODE_ZEROS_49121_BYTES, + "max_size_zeros", + "max_size_ones", + "over_limit_zeros", + "over_limit_ones", + "empty", + "single_byte", + "32_bytes", + "33_bytes", + "max_size_minus_word", + "max_size_minus_word_plus_byte", ], - ids=get_initcode_name, ) @pytest.mark.parametrize("opcode", [Op.CREATE, Op.CREATE2], ids=get_create_id) class TestCreateInitcode: @@ -516,23 +525,16 @@ def tx( ) @pytest.fixture - def contract_creation_gas_cost(self, fork: Fork, opcode: Op) -> int: + def contract_creation_gas_cost( + self, fork: Fork, opcode: Op, create2_salt: int + ) -> int: """Calculate gas cost of the contract creation operation.""" - gas_costs = fork.gas_costs() - - create_contract_base_gas = gas_costs.G_CREATE - gas_opcode_gas = gas_costs.G_BASE - push_dup_opcode_gas = gas_costs.G_VERY_LOW - calldatasize_opcode_gas = gas_costs.G_BASE - contract_creation_gas_usage = ( - create_contract_base_gas - + gas_opcode_gas - + (2 * push_dup_opcode_gas) - + calldatasize_opcode_gas + create_code = ( + opcode(size=Op.CALLDATASIZE, salt=create2_salt) + if opcode == Op.CREATE2 + else opcode(size=Op.CALLDATASIZE) ) - if opcode == Op.CREATE2: # Extra push operation - contract_creation_gas_usage += push_dup_opcode_gas - return contract_creation_gas_usage + return (create_code + Op.GAS).gas_cost(fork) @pytest.fixture def initcode_word_cost(self, fork: Fork, initcode: Initcode) -> int: @@ -569,6 +571,7 @@ def test_create_opcode_initcode( contract_creation_gas_cost: int, initcode_word_cost: int, create2_word_cost: int, + fork: Fork, ) -> None: """ Test contract creation with valid and invalid initcode lengths. @@ -576,7 +579,7 @@ def test_create_opcode_initcode( Test contract creation via CREATE/CREATE2, parametrized by initcode that is on/over the max allowed limit. """ - if len(initcode) > Spec.MAX_INITCODE_SIZE: + if len(initcode) > fork.max_initcode_size(): # Call returns 0 as out of gas s[0]==1 post[caller_contract_address] = Account( nonce=1, diff --git a/tests/tangerine_whistle/eip150_operation_gas_costs/test_eip150_selfdestruct.py b/tests/tangerine_whistle/eip150_operation_gas_costs/test_eip150_selfdestruct.py index df3674129b..9bd855017f 100644 --- a/tests/tangerine_whistle/eip150_operation_gas_costs/test_eip150_selfdestruct.py +++ b/tests/tangerine_whistle/eip150_operation_gas_costs/test_eip150_selfdestruct.py @@ -52,29 +52,24 @@ def calculate_selfdestruct_gas( originator_balance: int, ) -> int: """Calculate exact gas needed for SELFDESTRUCT.""" - gas_costs = fork.gas_costs() - gas = ( - # PUSH + SELFDESTRUCT - gas_costs.G_VERY_LOW + gas_costs.G_SELF_DESTRUCT - ) - - # Cold access cost (>=Berlin only) - if fork >= Berlin and not beneficiary_warm: - gas += gas_costs.G_COLD_ACCOUNT_ACCESS - # G_NEW_ACCOUNT: # - Pre-EIP-161 (TangerineWhistle): charged when beneficiary is dead # - Post-EIP-161 (>=SpuriousDragon): charged when beneficiary is dead # AND originator has balance > 0 + needs_new_account = False if beneficiary_dead: if fork >= SpuriousDragon: - if originator_balance > 0: - gas += gas_costs.G_NEW_ACCOUNT + needs_new_account = originator_balance > 0 else: # Pre-EIP-161: always charged when beneficiary is dead - gas += gas_costs.G_NEW_ACCOUNT + needs_new_account = True - return gas + # PUSH + SELFDESTRUCT (with metadata for warm/cold and new account) + return Op.SELFDESTRUCT( + 0, # beneficiary address (generates a PUSH) + address_warm=beneficiary_warm or fork < Berlin, + account_new=needs_new_account, + ).gas_cost(fork) def setup_selfdestruct_test( @@ -491,10 +486,11 @@ def test_selfdestruct_state_access_boundary( # Calculate gas for state access boundary only (base + cold access) # Does NOT include G_NEW_ACCOUNT - gas_costs = fork.gas_costs() - inner_call_gas = gas_costs.G_VERY_LOW + gas_costs.G_SELF_DESTRUCT - if fork >= Berlin and not warm: - inner_call_gas += gas_costs.G_COLD_ACCOUNT_ACCESS + inner_call_gas = Op.SELFDESTRUCT( + 0, # beneficiary address (generates a PUSH) + address_warm=warm or fork < Berlin, + account_new=False, + ).gas_cost(fork) if not is_success: inner_call_gas -= 1 @@ -576,20 +572,8 @@ def test_selfdestruct_state_access_boundary( @pytest.mark.parametrize( "beneficiary_initial_balance", [ - pytest.param( - 0, - id="dead_beneficiary", - marks=pytest.mark.pre_alloc_group( - "eip150_selfdestruct_precompile_dead" - ), - ), - pytest.param( - 1, - id="alive_beneficiary", - marks=pytest.mark.pre_alloc_group( - "eip150_selfdestruct_precompile_alive" - ), - ), + pytest.param(0, id="dead_beneficiary"), + pytest.param(1, id="alive_beneficiary"), ], ) @pytest.mark.valid_from("TangerineWhistle") @@ -697,20 +681,8 @@ def test_selfdestruct_to_precompile( @pytest.mark.parametrize( "beneficiary_initial_balance", [ - pytest.param( - 0, - id="dead_beneficiary", - marks=pytest.mark.pre_alloc_group( - "eip150_selfdestruct_precompile_boundary_dead" - ), - ), - pytest.param( - 1, - id="alive_beneficiary", - marks=pytest.mark.pre_alloc_group( - "eip150_selfdestruct_precompile_boundary_alive" - ), - ), + pytest.param(0, id="dead_beneficiary"), + pytest.param(1, id="alive_beneficiary"), ], ) @pytest.mark.valid_from("TangerineWhistle") @@ -740,8 +712,10 @@ def test_selfdestruct_to_precompile_state_access_boundary( beneficiary_dead = beneficiary_initial_balance == 0 # State access boundary: base cost only (no G_NEW_ACCOUNT) - gas_costs = fork.gas_costs() - inner_call_gas = gas_costs.G_VERY_LOW + gas_costs.G_SELF_DESTRUCT + # Precompiles are always warm + inner_call_gas = Op.SELFDESTRUCT( + 0, address_warm=True, account_new=False + ).gas_cost(fork) if not is_success: inner_call_gas -= 1 @@ -994,9 +968,9 @@ def test_selfdestruct_to_self( victim_code = Op.SELFDESTRUCT(Op.ADDRESS) # Gas: ADDRESS + SELFDESTRUCT (no cold access, no G_NEW_ACCOUNT) - # Note: ADDRESS opcode costs G_BASE, not G_VERY_LOW like PUSH - gas_costs = fork.gas_costs() - base_gas = gas_costs.G_BASE + gas_costs.G_SELF_DESTRUCT + base_gas = Op.SELFDESTRUCT( + Op.ADDRESS, address_warm=True, account_new=False + ).gas_cost(fork) inner_call_gas = base_gas if is_success else base_gas - 1 if same_tx: diff --git a/tox.ini b/tox.ini index 8ecc63d37c..8a4e6d3fbb 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ changedir = {toxinidir}/packages/testing package = editable commands = pytest \ - -n auto --maxprocesses 6 \ + -n {env:PYTEST_XDIST_AUTO_NUM_WORKERS:6} \ --basetemp="{temp_dir}/pytest" \ --ignore=src/execution_testing/cli/pytest_commands/plugins/filler/tests/test_benchmarking.py \ {posargs} \ @@ -92,7 +92,8 @@ description = Fill the tests using EELS (with Python) commands = fill \ -m "not slow and not benchmark" \ - -n auto --maxprocesses 10 --dist=loadgroup \ + # loadgroup: serialize xdist_group("bigmem") tests onto one worker to limit peak memory + -n {env:PYTEST_XDIST_AUTO_NUM_WORKERS:6} --dist=loadgroup \ --skip-index \ --cov-config=pyproject.toml \ --cov=ethereum \ @@ -124,7 +125,9 @@ commands = --show-capture=no \ --disable-warnings \ -m "not slow and not benchmark and not derived_test" \ - -n auto --maxprocesses 7 --dist=loadgroup \ + -n auto --maxprocesses 7 \ + # loadgroup: serialize xdist_group("bigmem") tests onto one worker to limit peak memory + --dist=loadgroup \ --basetemp="{temp_dir}/pytest" \ --log-to "{toxworkdir}/logs" \ --clean \ diff --git a/uv.lock b/uv.lock index cadbb4e817..96e300c5d8 100644 --- a/uv.lock +++ b/uv.lock @@ -870,6 +870,7 @@ dev = [ { name = "mkdocstrings-python" }, { name = "mypy" }, { name = "pillow" }, + { name = "psutil" }, { name = "pyflakes" }, { name = "pyspelling" }, { name = "pytest" }, @@ -978,6 +979,7 @@ dev = [ { name = "mkdocstrings-python", specifier = ">=1.0.0,<2" }, { name = "mypy", specifier = "==1.17.0" }, { name = "pillow", specifier = ">=10.0.1,<11" }, + { name = "psutil", specifier = ">=7.2.2" }, { name = "pyflakes", specifier = ">=3.0" }, { name = "pyspelling", specifier = ">=2.8.2,<3" }, { name = "pytest", specifier = ">=8,<9" }, @@ -1957,6 +1959,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "py-ecc" version = "8.0.0"