Skip to content

Commit f3996c0

Browse files
committed
feat(tests): start basic eip-7883 cases.
1 parent b0c9ced commit f3996c0

File tree

8 files changed

+432
-4
lines changed

8 files changed

+432
-4
lines changed

eels_resolutions.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@
3535
"same_as": "EELSMaster"
3636
},
3737
"Prague": {
38-
"git_url": "https://github.com/marioevz/execution-specs.git",
39-
"branch": "forks/prague",
40-
"commit": "bb0eb750d643ced0ebf5dec732cdd23558d0b7f2"
38+
"git_url": "https://github.com/marioevz/execution-specs.git",
39+
"branch": "forks/prague",
40+
"commit": "bb0eb750d643ced0ebf5dec732cdd23558d0b7f2"
41+
},
42+
"Osaka": {
43+
"git_url": "https://github.com/spencer-tb/execution-specs.git",
44+
"branch": "forks/osaka",
45+
"commit": "e59c6e3eaed0dbbca639b6f5b6acaa832e51ca00"
4146
}
4247
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
abstract: Tests [EIP-7883: ModExp Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-7883)
3+
Test cases for [EIP-7883: ModExp Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-7883).
4+
"""
5+
6+
MODEXP_GAS_INCREASE_FORK_NAME = "Osaka"
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
"""Shared pytest definitions for EIP-7883 tests."""
2+
3+
from typing import Dict, Tuple
4+
5+
import pytest
6+
7+
from ethereum_test_tools import (
8+
EOA,
9+
Account,
10+
Address,
11+
Alloc,
12+
Bytecode,
13+
CodeGasMeasure,
14+
Environment,
15+
Storage,
16+
Transaction,
17+
)
18+
from ethereum_test_tools.vm.opcode import Opcodes as Op
19+
20+
from .helpers import parse_modexp_input
21+
from .spec import Spec
22+
23+
24+
@pytest.fixture
25+
def env() -> Environment:
26+
"""Environment fixture."""
27+
return Environment()
28+
29+
30+
@pytest.fixture
31+
def parsed_input(input_data: bytes) -> Tuple[bytes, bytes, bytes, int]:
32+
"""Parse the ModExp input data."""
33+
return parse_modexp_input(input_data)
34+
35+
36+
@pytest.fixture
37+
def base(parsed_input: Tuple[bytes, bytes, bytes, int]) -> bytes:
38+
"""Get the base value from the parsed input."""
39+
return parsed_input[0]
40+
41+
42+
@pytest.fixture
43+
def exponent_bytes(parsed_input: Tuple[bytes, bytes, bytes, int]) -> bytes:
44+
"""Get the exponent bytes from the parsed input."""
45+
return parsed_input[1]
46+
47+
48+
@pytest.fixture
49+
def modulus(parsed_input: Tuple[bytes, bytes, bytes, int]) -> bytes:
50+
"""Get the modulus value from the parsed input."""
51+
return parsed_input[2]
52+
53+
54+
@pytest.fixture
55+
def exponent(parsed_input: Tuple[bytes, bytes, bytes, int]) -> int:
56+
"""Get the exponent value from the parsed input."""
57+
return parsed_input[3]
58+
59+
60+
@pytest.fixture
61+
def sender(pre: Alloc) -> EOA:
62+
"""Create and fund an EOA to be used as the transaction sender."""
63+
return pre.fund_eoa()
64+
65+
66+
@pytest.fixture
67+
def call_opcode() -> Op:
68+
"""Return default call used to call the precompile."""
69+
return Op.CALL
70+
71+
72+
@pytest.fixture
73+
def modexp_call_code(call_opcode: Op, input_data: bytes) -> Bytecode:
74+
"""Create bytecode to call the ModExp precompile."""
75+
call_code = call_opcode(
76+
address=Spec.MODEXP_ADDRESS,
77+
value=0,
78+
args_offset=0,
79+
args_size=Op.CALLDATASIZE,
80+
ret_offset=0,
81+
ret_size=0x80,
82+
)
83+
call_code += Op.SSTORE(0, Op.ISZERO(Op.ISZERO))
84+
return call_code
85+
86+
87+
@pytest.fixture
88+
def gas_measure_contract(pre: Alloc, modexp_call_code: Bytecode) -> Address:
89+
"""Deploys a contract that measures ModExp gas consumption."""
90+
calldata_copy = Op.CALLDATACOPY(0, 0, Op.CALLDATASIZE)
91+
measured_code = CodeGasMeasure(
92+
code=calldata_copy + modexp_call_code,
93+
overhead_cost=12, # TODO: Calculate overhead cost
94+
extra_stack_items=0,
95+
sstore_key=1,
96+
stop=True,
97+
)
98+
return pre.deploy_contract(measured_code)
99+
100+
101+
@pytest.fixture
102+
def precompile_gas_modifier() -> int:
103+
"""Modify the gas passed to the precompile, for negative testing purposes."""
104+
return 0
105+
106+
107+
@pytest.fixture
108+
def precompile_gas(base: bytes, exponent_bytes: bytes, modulus: bytes, expected_gas: int) -> int:
109+
"""Calculate gas cost for the ModExp precompile and verify it matches expected gas."""
110+
base_length = len(base)
111+
exponent_length = len(exponent_bytes)
112+
modulus_length = len(modulus)
113+
exponent_value = int.from_bytes(exponent_bytes, byteorder="big")
114+
calculated_gas = Spec.calculate_new_gas_cost(
115+
base_length, modulus_length, exponent_length, exponent_value
116+
)
117+
assert (
118+
calculated_gas == expected_gas
119+
), f"Calculated gas {calculated_gas} != Vector gas {expected_gas}"
120+
return calculated_gas
121+
122+
123+
@pytest.fixture
124+
def modexp_input_data(input_data: bytes) -> bytes:
125+
"""ModExp input data, directly use the input from the test vector."""
126+
return input_data
127+
128+
129+
@pytest.fixture
130+
def tx(
131+
sender: EOA,
132+
gas_measure_contract: Address,
133+
modexp_input_data: bytes,
134+
) -> Transaction:
135+
"""Transaction to measure gas consumption of the ModExp precompile."""
136+
return Transaction(
137+
sender=sender,
138+
to=gas_measure_contract,
139+
data=modexp_input_data,
140+
gas_limit=1_000_000,
141+
)
142+
143+
144+
@pytest.fixture
145+
def post(
146+
gas_measure_contract: Address,
147+
precompile_gas: int,
148+
) -> Dict[Address, Dict[str, Storage]]:
149+
"""Return expected post state with gas consumption check."""
150+
return {
151+
gas_measure_contract: Account(
152+
storage={
153+
0: 1, # Call should succeed
154+
1: precompile_gas,
155+
}
156+
)
157+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Helper functions for the EIP-7883 ModExp gas cost increase tests."""
2+
3+
import json
4+
import os
5+
from typing import List, Tuple
6+
7+
import pytest
8+
9+
10+
def current_python_script_directory(*args: str) -> str:
11+
"""Get the current Python script directory."""
12+
return os.path.join(os.path.dirname(os.path.realpath(__file__)), *args)
13+
14+
15+
def vectors_from_file(filename: str) -> List[Tuple]:
16+
"""Load test vectors from a file."""
17+
with open(current_python_script_directory(filename), "r") as f:
18+
vectors_json = json.load(f)
19+
result = []
20+
for vector in vectors_json:
21+
input_hex = vector["Input"]
22+
name = vector["Name"]
23+
gas_new = vector["GasNew"]
24+
param = pytest.param(
25+
bytes.fromhex(input_hex),
26+
gas_new,
27+
id=name,
28+
)
29+
result.append(param)
30+
return result
31+
32+
33+
def parse_modexp_input(input_data: bytes) -> Tuple[bytes, bytes, bytes, int]:
34+
"""Parse ModExp input data into base, exponent bytes, modulus, and exponent value."""
35+
base_length = int.from_bytes(input_data[0:32], byteorder="big")
36+
exponent_length = int.from_bytes(input_data[32:64], byteorder="big")
37+
modulus_length = int.from_bytes(input_data[64:96], byteorder="big")
38+
39+
base_start = 96
40+
base_end = base_start + base_length
41+
base = input_data[base_start:base_end]
42+
43+
exponent_start = base_end
44+
exponent_end = exponent_start + exponent_length
45+
exponent_bytes = input_data[exponent_start:exponent_end]
46+
exponent_value = int.from_bytes(exponent_bytes, byteorder="big")
47+
48+
modulus_start = exponent_end
49+
modulus_end = modulus_start + modulus_length
50+
modulus = input_data[modulus_start:modulus_end]
51+
52+
return base, exponent_bytes, modulus, exponent_value
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Defines EIP-7883 specification constants and functions."""
2+
3+
import math
4+
from dataclasses import dataclass
5+
6+
7+
@dataclass(frozen=True)
8+
class ReferenceSpec:
9+
"""Defines the reference spec version and git path."""
10+
11+
git_path: str
12+
version: str
13+
14+
15+
ref_spec_7883 = ReferenceSpec("EIPS/eip-7883.md", "13aa65810336d4f243d4563a828d5afe36035d23")
16+
17+
18+
@dataclass(frozen=True)
19+
class Spec:
20+
"""Constants and helpers for the ModExp gas cost increase EIP."""
21+
22+
MODEXP_ADDRESS = 0x05
23+
OLD_MIN_GAS = 200
24+
OLD_EXPONENT_BYTE_MULTIPLIER = 8
25+
NEW_MIN_GAS = 500
26+
NEW_EXPONENT_BYTE_MULTIPLIER = 16
27+
NEW_LARGE_BASE_MODULUS_MULTIPLIER = 2
28+
WORD_SIZE = 8
29+
BASE_MODULUS_THRESHOLD = 32
30+
EXPONENT_THRESHOLD = 32
31+
GAS_DIVISOR = 3
32+
33+
@staticmethod
34+
def calculate_new_gas_cost(
35+
base_length: int, modulus_length: int, exponent_length: int, exponent: int
36+
) -> int:
37+
"""Calculate the ModExp gas cost according to EIP-7883 specification."""
38+
max_length = max(base_length, modulus_length)
39+
words = math.ceil(max_length / Spec.WORD_SIZE)
40+
if max_length <= Spec.BASE_MODULUS_THRESHOLD:
41+
multiplication_complexity = words**2
42+
else:
43+
multiplication_complexity = 2 * words**2
44+
if exponent_length <= Spec.EXPONENT_THRESHOLD:
45+
if exponent == 0:
46+
iteration_count = 0
47+
else:
48+
iteration_count = exponent.bit_length() - 1
49+
else:
50+
high_bytes = exponent_length - Spec.EXPONENT_THRESHOLD
51+
low_bits = (exponent & (2**256 - 1)).bit_length() - 1
52+
iteration_count = (Spec.NEW_EXPONENT_BYTE_MULTIPLIER * high_bytes) + low_bits
53+
iteration_count = max(iteration_count, 1)
54+
gas_cost = (multiplication_complexity * iteration_count) // Spec.GAS_DIVISOR
55+
return max(Spec.NEW_MIN_GAS, gas_cost)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
abstract: Tests [EIP-7883: ModExp Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-7883)
3+
Test cases for [EIP-7883: ModExp Gas Cost Increase](https://eips.ethereum.org/EIPS/eip-7883).
4+
"""
5+
6+
from typing import Dict
7+
8+
import pytest
9+
10+
from ethereum_test_tools import (
11+
Alloc,
12+
Environment,
13+
StateTestFiller,
14+
Transaction,
15+
)
16+
17+
from . import MODEXP_GAS_INCREASE_FORK_NAME
18+
from .helpers import vectors_from_file
19+
from .spec import ref_spec_7883
20+
21+
REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path
22+
REFERENCE_SPEC_VERSION = ref_spec_7883.version
23+
24+
pytestmark = pytest.mark.valid_from(MODEXP_GAS_INCREASE_FORK_NAME)
25+
26+
27+
@pytest.mark.parametrize("input_data,expected_gas", vectors_from_file("vectors.json"))
28+
def test_vectors_from_file(
29+
expected_gas: int,
30+
state_test: StateTestFiller,
31+
env: Environment,
32+
pre: Alloc,
33+
tx: Transaction,
34+
post: Dict,
35+
):
36+
"""Test ModExp gas cost using the test vectors from EIP-7883."""
37+
state_test(
38+
env=env,
39+
pre=pre,
40+
tx=tx,
41+
post=post,
42+
)

0 commit comments

Comments
 (0)