Skip to content

Commit 7708810

Browse files
feat(tests): add transition test and update helper
1 parent b3cc636 commit 7708810

File tree

3 files changed

+375
-78
lines changed

3 files changed

+375
-78
lines changed
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Conftest for EIP-7823 tests."""
2+
3+
from typing import Dict
4+
5+
import pytest
6+
7+
from ethereum_test_forks import Fork, Osaka
8+
from ethereum_test_tools import Account, Address, Alloc, Bytes, Storage, Transaction, keccak256
9+
from ethereum_test_tools.vm.opcode import Opcodes as Op
10+
from ethereum_test_types import Environment
11+
12+
from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput
13+
from ..eip7883_modexp_gas_increase.spec import Spec as Spec7883
14+
from .spec import Spec
15+
16+
17+
@pytest.fixture
18+
def call_contract_post_storage() -> Storage:
19+
"""
20+
Storage of the test contract after the transaction is executed.
21+
Note: Fixture `call_contract_code` fills the actual expected storage values.
22+
"""
23+
return Storage()
24+
25+
26+
@pytest.fixture
27+
def call_succeeds(
28+
total_gas_used: int, fork: Fork, env: Environment, modexp_input: ModExpInput
29+
) -> bool:
30+
"""
31+
By default, depending on the expected output, we can deduce if the call is expected to succeed
32+
or fail.
33+
"""
34+
# Transaction gas limit exceeded
35+
tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit
36+
if total_gas_used > tx_gas_limit_cap:
37+
return False
38+
39+
# Input length exceeded
40+
base_length, exp_length, mod_length = modexp_input.get_declared_lengths()
41+
if (
42+
base_length > Spec.MAX_LENGTH_BYTES
43+
or exp_length > Spec.MAX_LENGTH_BYTES
44+
or mod_length > Spec.MAX_LENGTH_BYTES
45+
) and fork >= Osaka:
46+
return False
47+
48+
return True
49+
50+
51+
@pytest.fixture
52+
def gas_measure_contract(
53+
pre: Alloc,
54+
fork: Fork,
55+
modexp_expected: bytes,
56+
precompile_gas: int,
57+
precompile_gas_modifier: int,
58+
call_contract_post_storage: Storage,
59+
call_succeeds: bool,
60+
) -> Address:
61+
"""
62+
Deploys a contract that measures ModExp gas consumption and execution result.
63+
64+
Always stored:
65+
storage[0]: precompile call success
66+
storage[1]: return data length from precompile
67+
Only if the precompile call succeeds:
68+
storage[2]: gas consumed by precompile
69+
storage[3]: hash of return data from precompile
70+
"""
71+
call_code = Op.CALL(
72+
precompile_gas + precompile_gas_modifier,
73+
Spec.MODEXP_ADDRESS,
74+
0,
75+
0,
76+
Op.CALLDATASIZE(),
77+
0,
78+
0,
79+
)
80+
81+
gas_costs = fork.gas_costs()
82+
extra_gas = (
83+
gas_costs.G_WARM_ACCOUNT_ACCESS
84+
+ (gas_costs.G_VERY_LOW * (len(Op.CALL.kwargs) - 2)) # type: ignore
85+
+ gas_costs.G_BASE # CALLDATASIZE
86+
+ gas_costs.G_BASE # GAS
87+
)
88+
89+
# Build the gas measurement contract code
90+
# Stack operations:
91+
# [gas_start]
92+
# [gas_start, call_result]
93+
# [gas_start, call_result, gas_end]
94+
# [gas_start, gas_end, call_result]
95+
call_result_measurement = Op.GAS + call_code + Op.GAS + Op.SWAP1
96+
97+
# Calculate gas consumed: gas_start - (gas_end + extra_gas)
98+
# Stack Operation:
99+
# [gas_start, gas_end]
100+
# [gas_start, gas_end, extra_gas]
101+
# [gas_start, gas_end + extra_gas]
102+
# [gas_end + extra_gas, gas_start]
103+
# [gas_consumed]
104+
gas_calculation = Op.PUSH2[extra_gas] + Op.ADD + Op.SWAP1 + Op.SUB
105+
106+
code = (
107+
Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE)
108+
+ Op.SSTORE(call_contract_post_storage.store_next(call_succeeds), call_result_measurement)
109+
+ Op.SSTORE(
110+
call_contract_post_storage.store_next(len(modexp_expected) if call_succeeds else 0),
111+
Op.RETURNDATASIZE(),
112+
)
113+
)
114+
115+
if call_succeeds:
116+
code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation)
117+
code += Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE())
118+
code += Op.SSTORE(
119+
call_contract_post_storage.store_next(keccak256(Bytes(modexp_expected))),
120+
Op.SHA3(0, Op.RETURNDATASIZE()),
121+
)
122+
return pre.deploy_contract(code)
123+
124+
125+
@pytest.fixture
126+
def precompile_gas(fork: Fork, modexp_input: ModExpInput) -> int:
127+
"""Calculate gas cost for the ModExp precompile and verify it matches expected gas."""
128+
spec = Spec if fork < Osaka else Spec7883
129+
try:
130+
calculated_gas = spec.calculate_gas_cost(modexp_input)
131+
return calculated_gas
132+
except Exception:
133+
# Used for `test_modexp_invalid_inputs` we expect the call to not succeed.
134+
# Return is for completeness.
135+
return 500 if fork >= Osaka else 200
136+
137+
138+
@pytest.fixture
139+
def precompile_gas_modifier() -> int:
140+
"""Return the gas modifier for the ModExp precompile."""
141+
return 0
142+
143+
144+
@pytest.fixture
145+
def tx(
146+
pre: Alloc,
147+
gas_measure_contract: Address,
148+
modexp_input: ModExpInput,
149+
tx_gas_limit: int,
150+
) -> Transaction:
151+
"""Transaction to measure gas consumption of the ModExp precompile."""
152+
return Transaction(
153+
sender=pre.fund_eoa(),
154+
to=gas_measure_contract,
155+
data=bytes(modexp_input),
156+
gas_limit=tx_gas_limit,
157+
)
158+
159+
160+
@pytest.fixture
161+
def total_gas_used(
162+
fork: Fork, modexp_expected: bytes, modexp_input: ModExpInput, precompile_gas: int
163+
) -> int:
164+
"""Transaction gas limit used for the test (Can be overridden in the test)."""
165+
intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
166+
memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator()
167+
extra_gas = 500_000
168+
169+
total_gas = (
170+
extra_gas
171+
+ intrinsic_gas_cost_calculator(calldata=bytes(modexp_input))
172+
+ memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input)))
173+
+ precompile_gas
174+
)
175+
176+
return total_gas
177+
178+
179+
@pytest.fixture
180+
def tx_gas_limit(total_gas_used: int, fork: Fork, env: Environment) -> int:
181+
"""Transaction gas limit used for the test (Can be overridden in the test)."""
182+
tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit
183+
return min(tx_gas_limit_cap, total_gas_used)
184+
185+
186+
@pytest.fixture
187+
def post(
188+
gas_measure_contract: Address,
189+
call_contract_post_storage: Storage,
190+
) -> Dict[Address, Account]:
191+
"""Return expected post state with gas consumption check."""
192+
return {
193+
gas_measure_contract: Account(storage=call_contract_post_storage),
194+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Defines EIP-7823 specification constants and functions."""
2+
3+
from dataclasses import dataclass
4+
5+
from ..eip7883_modexp_gas_increase.spec import Spec, ceiling_division
6+
7+
8+
@dataclass(frozen=True)
9+
class ReferenceSpec:
10+
"""Defines the reference spec version and git path."""
11+
12+
git_path: str
13+
version: str
14+
15+
16+
ref_spec_7823 = ReferenceSpec("EIPS/eip-7823.md", "c8321494fdfbfda52ad46c3515a7ca5dc86b857c")
17+
18+
19+
@dataclass(frozen=True)
20+
class Spec7823(Spec):
21+
"""
22+
Constants and helpers for the ModExp gas cost increase EIP.
23+
These override the original Spec class variables for EIP-7823.
24+
"""
25+
26+
MODEXP_ADDRESS = 0x05
27+
MIN_GAS = 500
28+
29+
LARGE_BASE_MODULUS_MULTIPLIER = 2
30+
EXPONENT_BYTE_MULTIPLIER = 16
31+
GAS_DIVISOR = 1 # Overrides the original Spec class GAS_DIVISOR
32+
33+
@classmethod
34+
def calculate_multiplication_complexity(cls, base_length: int, modulus_length: int) -> int:
35+
"""Calculate the multiplication complexity of the ModExp precompile for EIP-7883."""
36+
max_length = max(base_length, modulus_length)
37+
words = ceiling_division(max_length, cls.WORD_SIZE)
38+
complexity = 16
39+
if max_length > cls.MAX_LENGTH_THRESHOLD:
40+
complexity = cls.LARGE_BASE_MODULUS_MULTIPLIER * words**2
41+
return complexity

0 commit comments

Comments
 (0)