Skip to content

feat(tests): add EIP-7951 for secp256r1 curve #1670

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 18 commits into from
Jun 10, 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 @@ -64,6 +64,7 @@ Users can select any of the artifacts depending on their testing needs for their
- 🔀 Refactored EIP-145 static tests into python ([#1683](https://github.com/ethereum/execution-spec-tests/pull/1683)).
- ✨ EIP-7823, EIP-7883: Add test cases for ModExp precompile gas-cost updates and input limits on Osaka ([#1579](https://github.com/ethereum/execution-spec-tests/pull/1579), [#1729](https://github.com/ethereum/execution-spec-tests/pull/1729)).
- ✨ [EIP-7825](https://eips.ethereum.org/EIPS/eip-7825): Add test cases for the transaction gas limit of 30M gas ([#1711](https://github.com/ethereum/execution-spec-tests/pull/1711)).
- ✨ [EIP-7951](https://eips.ethereum.org/EIPS/eip-7951): add test cases for `P256VERIFY` precompile to support secp256r1 curve [#1670](https://github.com/ethereum/execution-spec-tests/pull/1670)

## [v4.5.0](https://github.com/ethereum/execution-spec-tests/releases/tag/v4.5.0) - 2025-05-14

Expand Down
9 changes: 9 additions & 0 deletions src/ethereum_test_forks/forks/forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,15 @@ def solc_min_version(cls) -> Version:
"""Return minimum version of solc that supports this fork."""
return Version.parse("1.0.0") # set a high version; currently unknown

@classmethod
def precompiles(cls, block_number: int = 0, timestamp: int = 0) -> List[Address]:
"""
At Osaka, pre-compile for p256verify operation is added.

P256VERIFY = 0x100
"""
return [Address(0x100)] + super(Osaka, cls).precompiles(block_number, timestamp)


class EOFv1(Prague, solc_name="cancun"):
"""EOF fork."""
Expand Down
53 changes: 52 additions & 1 deletion src/ethereum_test_forks/tests/test_forks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Homestead,
Istanbul,
London,
Osaka,
Paris,
Prague,
Shanghai,
Expand All @@ -23,6 +24,7 @@
BerlinToLondonAt5,
CancunToPragueAtTime15k,
ParisToShanghaiAtTime15k,
PragueToOsakaAtTime15k,
ShanghaiToCancunAtTime15k,
)
from ..helpers import (
Expand Down Expand Up @@ -223,6 +225,7 @@ def test_transition_fork_comparison():

assert sorted(
{
PragueToOsakaAtTime15k,
CancunToPragueAtTime15k,
ParisToShanghaiAtTime15k,
ShanghaiToCancunAtTime15k,
Expand All @@ -233,6 +236,7 @@ def test_transition_fork_comparison():
ParisToShanghaiAtTime15k,
ShanghaiToCancunAtTime15k,
CancunToPragueAtTime15k,
PragueToOsakaAtTime15k,
]


Expand Down Expand Up @@ -349,7 +353,7 @@ def test_tx_intrinsic_gas_functions(fork: Fork, calldata: bytes, create_tx: bool
)


class FutureFork(Prague):
class FutureFork(Osaka):
"""
Dummy fork used for testing.

Expand Down Expand Up @@ -391,6 +395,27 @@ class FutureFork(Prague):
},
id="Prague",
),
pytest.param(
Osaka,
{
"Cancun": {
"target_blobs_per_block": 3,
"max_blobs_per_block": 6,
"baseFeeUpdateFraction": 3338477,
},
"Prague": {
"target_blobs_per_block": 6,
"max_blobs_per_block": 9,
"baseFeeUpdateFraction": 5007716,
},
"Osaka": {
"target_blobs_per_block": 6,
"max_blobs_per_block": 9,
"baseFeeUpdateFraction": 5007716,
},
},
id="Osaka",
),
pytest.param(
CancunToPragueAtTime15k,
{
Expand All @@ -407,6 +432,27 @@ class FutureFork(Prague):
},
id="CancunToPragueAtTime15k",
),
pytest.param(
PragueToOsakaAtTime15k,
{
"Cancun": {
"target_blobs_per_block": 3,
"max_blobs_per_block": 6,
"baseFeeUpdateFraction": 3338477,
},
"Prague": {
"target_blobs_per_block": 6,
"max_blobs_per_block": 9,
"baseFeeUpdateFraction": 5007716,
},
"Osaka": {
"target_blobs_per_block": 6,
"max_blobs_per_block": 9,
"baseFeeUpdateFraction": 5007716,
},
},
id="PragueToOsakaAtTime15k",
),
pytest.param(
FutureFork,
{
Expand All @@ -420,6 +466,11 @@ class FutureFork(Prague):
"max_blobs_per_block": 9,
"baseFeeUpdateFraction": 5007716,
},
"Osaka": {
"target_blobs_per_block": 6,
"max_blobs_per_block": 9,
"baseFeeUpdateFraction": 5007716,
},
"FutureFork": {
"target_blobs_per_block": 6,
"max_blobs_per_block": 9,
Expand Down
21 changes: 9 additions & 12 deletions tests/frontier/precompiles/test_precompiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@
from ethereum_test_forks import Fork
from ethereum_test_tools import (
Account,
Address,
Alloc,
Environment,
StateTestFiller,
Transaction,
)
from ethereum_test_tools.vm.opcode import Opcodes as Op

UPPER_BOUND = 0xFF
NUM_UNSUPPORTED_PRECOMPILES = 1


def precompile_addresses(fork: Fork) -> Iterator[Tuple[str, bool]]:
def precompile_addresses(fork: Fork) -> Iterator[Tuple[Address, bool]]:
"""
Yield the addresses of precompiled contracts and their support status for a given fork.

Expand All @@ -32,14 +30,13 @@ def precompile_addresses(fork: Fork) -> Iterator[Tuple[str, bool]]:
"""
supported_precompiles = fork.precompiles()

num_unsupported = NUM_UNSUPPORTED_PRECOMPILES
for address in range(1, UPPER_BOUND + 1):
if address in supported_precompiles:
yield (hex(address), True)
elif num_unsupported > 0:
# Check unsupported precompiles up to NUM_UNSUPPORTED_PRECOMPILES
yield (hex(address), False)
num_unsupported -= 1
for address in supported_precompiles:
address_int = int.from_bytes(address, byteorder="big")
yield (address, True)
if address_int > 0 and (address_int - 1) not in supported_precompiles:
yield (Address(address_int - 1), False)
if (address_int + 1) not in supported_precompiles:
yield (Address(address_int + 1), False)


@pytest.mark.ported_from(
Expand Down
4 changes: 4 additions & 0 deletions tests/osaka/eip7951_p256verify_precompiles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"""
abstract: Tests [EIP-7951: Precompile for secp256r1 Curve Support](https://eips.ethereum.org/EIPS/eip-7951)
Test cases for [EIP-7951: Precompile for secp256r1 Curve Support](https://eips.ethereum.org/EIPS/eip-7951)].
"""
161 changes: 161 additions & 0 deletions tests/osaka/eip7951_p256verify_precompiles/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""Shared pytest definitions local to EIP-7951 tests."""

from typing import SupportsBytes

import pytest

from ethereum_test_forks import Fork
from ethereum_test_tools import EOA, Address, Alloc, Bytecode, Storage, Transaction, keccak256
from ethereum_test_tools import Opcodes as Op

from .spec import Spec


@pytest.fixture
def vector_gas_value() -> int | None:
"""
Gas value from the test vector if any.

If `None` it means that the test scenario did not come from a file, so no comparison is needed.

The `vectors_from_file` function reads the gas value from the file and overwrites this fixture.
"""
return None


@pytest.fixture
def precompile_gas(vector_gas_value: int | None) -> int:
"""Gas cost for the precompile."""
if vector_gas_value is not None:
assert vector_gas_value == Spec.P256VERIFY_GAS, (
f"Calculated gas {vector_gas_value} != Vector gas {Spec.P256VERIFY_GAS}"
)
return Spec.P256VERIFY_GAS


@pytest.fixture
def precompile_gas_modifier() -> int:
"""
Modify the gas passed to the precompile, for testing purposes.

By default the call is made with the exact gas amount required for the given opcode,
but when this fixture is overridden, the gas amount can be modified to, e.g., test
a lower amount and test if the precompile call fails.
"""
return 0


@pytest.fixture
def call_opcode() -> Op:
"""
Type of call used to call the precompile.

By default it is Op.CALL, but it can be overridden in the test.
"""
return Op.CALL


@pytest.fixture
def call_contract_post_storage() -> Storage:
"""
Storage of the test contract after the transaction is executed.
Note: Fixture `call_contract_code` fills the actual expected storage values.
"""
return Storage()


@pytest.fixture
def call_succeeds() -> bool:
"""
By default, depending on the expected output, we can deduce if the call is expected to succeed
or fail.
"""
return True


@pytest.fixture
def call_contract_code(
precompile_address: int,
precompile_gas: int,
precompile_gas_modifier: int,
expected_output: bytes | SupportsBytes,
call_succeeds: bool,
call_opcode: Op,
call_contract_post_storage: Storage,
) -> Bytecode:
"""Code of the test contract."""
expected_output = bytes(expected_output)
assert call_opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL]
value = [0] if call_opcode in [Op.CALL, Op.CALLCODE] else []

code = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE()) + Op.SSTORE(
call_contract_post_storage.store_next(call_succeeds),
call_opcode(
precompile_gas + precompile_gas_modifier,
precompile_address,
*value,
0,
Op.CALLDATASIZE(),
0,
0,
)
+ Op.SSTORE(
call_contract_post_storage.store_next(len(expected_output)), Op.RETURNDATASIZE()
),
)
if call_succeeds:
# Add integrity check only if the call is expected to succeed.
code += Op.RETURNDATACOPY(0, 0, Op.RETURNDATASIZE()) + Op.SSTORE(
call_contract_post_storage.store_next(keccak256(expected_output)),
Op.SHA3(0, Op.RETURNDATASIZE()),
)
return code


@pytest.fixture
def call_contract_address(pre: Alloc, call_contract_code: Bytecode) -> Address:
"""Address where the test contract will be deployed."""
return pre.deploy_contract(call_contract_code)


@pytest.fixture
def sender(pre: Alloc) -> EOA:
"""Sender of the transaction."""
return pre.fund_eoa()


@pytest.fixture
def post(call_contract_address: Address, call_contract_post_storage: Storage):
"""Test expected post outcome."""
return {
call_contract_address: {
"storage": call_contract_post_storage,
},
}


@pytest.fixture
def tx_gas_limit(fork: Fork, input_data: bytes, precompile_gas: int) -> int:
"""Transaction gas limit used for the test (Can be overridden in the test)."""
intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator()
extra_gas = 100_000
return (
extra_gas
+ intrinsic_gas_cost_calculator(calldata=input_data)
+ memory_expansion_gas_calculator(new_bytes=len(input_data))
+ precompile_gas
)


@pytest.fixture
def tx(
input_data: bytes,
tx_gas_limit: int,
call_contract_address: Address,
sender: EOA,
) -> Transaction:
"""Transaction for the test."""
return Transaction(
gas_limit=tx_gas_limit, data=input_data, to=call_contract_address, sender=sender
)
Loading
Loading