Skip to content

feat(tests): add eip-2929 precompile address warming tests #1495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Users can expect that all tests currently living in [ethereum/tests](https://git
- ✨ [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702): Test that DELEGATECALL to a 7702 target works as intended ([#1485](https://github.com/ethereum/execution-spec-tests/pull/1485)).
- ✨ [EIP-2573](https://eips.ethereum.org/EIPS/eip-2537): Includes a BLS12 point generator, alongside additional coverage many of the precompiles ([#1350](https://github.com/ethereum/execution-spec-tests/pull/1350)).
- ✨ Add all [`GeneralStateTests` from `ethereum/tests`](https://github.com/ethereum/tests/tree/7dc757ec132e372b6178a016b91f4c639f366c02/src/GeneralStateTestsFiller) to `execution-spec-tests` located now at [tests/static/state_tests](https://github.com/ethereum/execution-spec-tests/tree/main/tests/static/state_tests) ([#1442](https://github.com/ethereum/execution-spec-tests/pull/1442)).
- ✨ [EIP-2929](https://eips.ethereum.org/EIPS/eip-2929): Test that precompile addresses are cold/warm depending on the fork they are activated ([#1495](https://github.com/ethereum/execution-spec-tests/pull/1495)).

## [v4.3.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.3.0) - 2025-04-18

Expand Down
2 changes: 1 addition & 1 deletion src/pytest_plugins/consume/hive_simulators/ruleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ def get_blob_schedule_entries(fork: Fork) -> Dict[str, int]:
"HIVE_TERMINAL_TOTAL_DIFFICULTY": 0,
"HIVE_SHANGHAI_TIMESTAMP": 0,
},
"MergeToShanghaiAtTime15k": {
"ParisToShanghaiAtTime15k": {
"HIVE_FORK_HOMESTEAD": 0,
"HIVE_FORK_TANGERINE": 0,
"HIVE_FORK_SPURIOUS": 0,
Expand Down
4 changes: 4 additions & 0 deletions tests/berlin/eip2929_gas_cost_increases/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
abstract: Tests [EIP-2929: Gas cost increases for state access opcodes](https://eips.ethereum.org/EIPS/eip-2929)
Test cases for [EIP-2929: Gas cost increases for state access opcodes](https://eips.ethereum.org/EIPS/eip-2929).
""" # noqa: E501
147 changes: 147 additions & 0 deletions tests/berlin/eip2929_gas_cost_increases/test_precompile_warming.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""
abstract: Tests [EIP-2929: Gas cost increases for state access opcodes](https://eips.ethereum.org/EIPS/eip-2929)
Test cases for [EIP-2929: Gas cost increases for state access opcodes](https://eips.ethereum.org/EIPS/eip-2929).
""" # noqa: E501

from typing import Iterator, Tuple

import pytest

from ethereum_test_forks import (
Fork,
get_transition_fork_predecessor,
get_transition_fork_successor,
)
from ethereum_test_tools import (
Account,
Address,
Alloc,
Block,
BlockchainTestFiller,
Transaction,
)
from ethereum_test_tools.vm.opcode import Opcodes as Op

REFERENCE_SPEC_GIT_PATH = "EIPS/eip-2929.md"
REFERENCE_SPEC_VERSION = "0e11417265a623adb680c527b15d0cb6701b870b"


def precompile_addresses_in_predecessor_successor(
fork: Fork,
) -> Iterator[Tuple[Address, bool, bool]]:
"""
Yield the addresses of precompiled contracts and whether they existed in the parent fork.

Args:
fork (Fork): The transition fork instance containing precompiled contract information.

Yields:
Iterator[Tuple[str, bool]]: A tuple containing the address in hexadecimal format and a
boolean indicating whether the address has existed in the predecessor.

"""
predecessor_precompiles = set(get_transition_fork_predecessor(fork).precompiles())
successor_precompiles = set(get_transition_fork_successor(fork).precompiles())
all_precompiles = successor_precompiles | predecessor_precompiles
highest_precompile = int.from_bytes(max(all_precompiles))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from_bytes requires two arguments, I had to change this line to:
highest_precompile: int = int.from_bytes(max(all_precompiles), sys.byteorder)
and import sys to make it work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you say make it work you mean fill?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just having the file opened in an editor shows the problem. But now I know it depends on which Python version you use:
python 3.11 -> optional parameter
python 3.10 -> mandatory parameter

So I think it would be good if we find an exact Python version we enforce in EEST (everyone uses the exact same) to avoid stuff like this in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1516

created an issue for discussion on enforcing the python version.

extra_range = 32
extra_precompiles = {
Address(i) for i in range(highest_precompile + 1, highest_precompile + extra_range)
}
all_precompiles = all_precompiles | extra_precompiles
for address in sorted(all_precompiles):
yield address, address in successor_precompiles, address in predecessor_precompiles


@pytest.mark.valid_at_transition_to("Paris", subsequent_forks=True)
@pytest.mark.parametrize_by_fork(
"address,precompile_in_successor,precompile_in_predecessor",
precompile_addresses_in_predecessor_successor,
)
def test_precompile_warming(
blockchain_test: BlockchainTestFiller,
fork: Fork,
address: Address,
precompile_in_successor: bool,
precompile_in_predecessor: bool,
pre: Alloc,
):
"""
Call BALANCE of a precompile addresses before and after a fork.

According to EIP-2929, when a transaction begins, accessed_addresses is initialized to include:
- tx.sender, tx.to
- and the set of all precompiles

This test verifies that:
1. Precompiles that exist in the predecessor fork are always "warm" (lower gas cost)
2. New precompiles added in a fork are "cold" before the fork and become "warm" after

"""
sender = pre.fund_eoa()
call_cost_slot = 0

code = (
Op.GAS
+ Op.BALANCE(address)
+ Op.POP
+ Op.SSTORE(call_cost_slot, Op.SUB(Op.SWAP1, Op.GAS))
+ Op.STOP
)
before = pre.deploy_contract(code, storage={call_cost_slot: 0xDEADBEEF})
after = pre.deploy_contract(code, storage={call_cost_slot: 0xDEADBEEF})

# Block before fork
blocks = [
Block(
timestamp=10_000,
txs=[
Transaction(
sender=sender,
to=before,
gas_limit=1_000_000,
)
],
)
]

# Block after fork
blocks += [
Block(
timestamp=20_000,
txs=[
Transaction(
sender=sender,
to=after,
gas_limit=1_000_000,
)
],
)
]

predecessor = get_transition_fork_predecessor(fork)
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

expected_gas_before = get_expected_gas(precompile_in_predecessor, predecessor)
expected_gas_after = get_expected_gas(precompile_in_successor, successor)

post = {
before: Account(storage={call_cost_slot: expected_gas_before}),
after: Account(storage={call_cost_slot: expected_gas_after}),
}

blockchain_test(
pre=pre,
post=post,
blocks=blocks,
)
Loading