Skip to content

Commit 110b5d4

Browse files
refactor(tests): add checklist marker for eip7823 (#2115)
* feat(tests): enhance eip7823 coverage * feat(tests): add eip7823 checklist items for coverage * refactor(tests): update modexp upper bounds framework * feat(tests): expand checklist item and enhance test coverage
1 parent 98848df commit 110b5d4

File tree

6 files changed

+452
-90
lines changed

6 files changed

+452
-90
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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, 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, Spec7883
14+
15+
16+
@pytest.fixture
17+
def call_contract_post_storage() -> Storage:
18+
"""
19+
Storage of the test contract after the transaction is executed.
20+
Note: Fixture `call_contract_code` fills the actual expected storage values.
21+
"""
22+
return Storage()
23+
24+
25+
@pytest.fixture
26+
def call_succeeds(
27+
total_gas_used: int, fork: Fork, env: Environment, modexp_input: ModExpInput
28+
) -> bool:
29+
"""
30+
By default, depending on the expected output, we can deduce if the call is expected to succeed
31+
or fail.
32+
"""
33+
# Transaction gas limit exceeded
34+
tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit
35+
if total_gas_used > tx_gas_limit_cap:
36+
return False
37+
38+
# Input length exceeded
39+
base_length, exp_length, mod_length = modexp_input.get_declared_lengths()
40+
if (
41+
base_length > Spec.MAX_LENGTH_BYTES
42+
or exp_length > Spec.MAX_LENGTH_BYTES
43+
or mod_length > Spec.MAX_LENGTH_BYTES
44+
) and fork >= Osaka:
45+
return False
46+
47+
return True
48+
49+
50+
@pytest.fixture
51+
def gas_measure_contract(
52+
pre: Alloc,
53+
fork: Fork,
54+
modexp_expected: bytes,
55+
precompile_gas: int,
56+
call_contract_post_storage: Storage,
57+
call_succeeds: bool,
58+
) -> Address:
59+
"""
60+
Deploys a contract that measures ModExp gas consumption and execution result.
61+
62+
Always stored:
63+
storage[0]: precompile call success
64+
storage[1]: return data length from precompile
65+
Only if the precompile call succeeds:
66+
storage[2]: gas consumed by precompile
67+
storage[3]: hash of return data from precompile
68+
"""
69+
call_code = Op.CALL(
70+
precompile_gas,
71+
Spec.MODEXP_ADDRESS,
72+
0,
73+
0,
74+
Op.CALLDATASIZE(),
75+
0,
76+
0,
77+
)
78+
79+
gas_costs = fork.gas_costs()
80+
extra_gas = (
81+
gas_costs.G_WARM_ACCOUNT_ACCESS
82+
+ (gas_costs.G_VERY_LOW * (len(Op.CALL.kwargs) - 1)) # type: ignore
83+
+ gas_costs.G_BASE # CALLDATASIZE
84+
+ gas_costs.G_BASE # GAS
85+
)
86+
87+
# Build the gas measurement contract code
88+
# Stack operations:
89+
# [gas_start]
90+
# [gas_start, call_result]
91+
# [gas_start, call_result, gas_end]
92+
# [gas_start, gas_end, call_result]
93+
call_result_measurement = Op.GAS + call_code + Op.GAS + Op.SWAP1
94+
95+
# Calculate gas consumed: gas_start - (gas_end + extra_gas)
96+
# Stack Operation:
97+
# [gas_start, gas_end]
98+
# [gas_start, gas_end, extra_gas]
99+
# [gas_start, gas_end + extra_gas]
100+
# [gas_end + extra_gas, gas_start]
101+
# [gas_consumed]
102+
gas_calculation = Op.PUSH2[extra_gas] + Op.ADD + Op.SWAP1 + Op.SUB
103+
104+
code = (
105+
Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE)
106+
+ Op.SSTORE(call_contract_post_storage.store_next(call_succeeds), call_result_measurement)
107+
+ Op.SSTORE(
108+
call_contract_post_storage.store_next(len(modexp_expected) if call_succeeds else 0),
109+
Op.RETURNDATASIZE(),
110+
)
111+
)
112+
113+
if call_succeeds:
114+
code += Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation)
115+
code += Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE())
116+
code += Op.SSTORE(
117+
call_contract_post_storage.store_next(keccak256(bytes(modexp_expected))),
118+
Op.SHA3(0, Op.RETURNDATASIZE()),
119+
)
120+
return pre.deploy_contract(code)
121+
122+
123+
@pytest.fixture
124+
def precompile_gas(fork: Fork, modexp_input: ModExpInput) -> int:
125+
"""Calculate gas cost for the ModExp precompile and verify it matches expected gas."""
126+
spec = Spec if fork < Osaka else Spec7883
127+
try:
128+
calculated_gas = spec.calculate_gas_cost(modexp_input)
129+
return calculated_gas
130+
except Exception:
131+
# Used for `test_modexp_invalid_inputs` we expect the call to not succeed.
132+
# Return is for completeness.
133+
return 500 if fork >= Osaka else 200
134+
135+
136+
@pytest.fixture
137+
def tx(
138+
pre: Alloc,
139+
gas_measure_contract: Address,
140+
modexp_input: ModExpInput,
141+
tx_gas_limit: int,
142+
) -> Transaction:
143+
"""Transaction to measure gas consumption of the ModExp precompile."""
144+
return Transaction(
145+
sender=pre.fund_eoa(),
146+
to=gas_measure_contract,
147+
data=bytes(modexp_input),
148+
gas_limit=tx_gas_limit,
149+
)
150+
151+
152+
@pytest.fixture
153+
def total_gas_used(
154+
fork: Fork, modexp_expected: bytes, modexp_input: ModExpInput, precompile_gas: int
155+
) -> int:
156+
"""Transaction gas limit used for the test (Can be overridden in the test)."""
157+
intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
158+
memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator()
159+
extra_gas = 500_000
160+
161+
total_gas = (
162+
extra_gas
163+
+ intrinsic_gas_cost_calculator(calldata=bytes(modexp_input))
164+
+ memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input)))
165+
+ precompile_gas
166+
)
167+
168+
return total_gas
169+
170+
171+
@pytest.fixture
172+
def tx_gas_limit(total_gas_used: int, fork: Fork, env: Environment) -> int:
173+
"""Transaction gas limit used for the test (Can be overridden in the test)."""
174+
tx_gas_limit_cap = fork.transaction_gas_limit_cap() or env.gas_limit
175+
return min(tx_gas_limit_cap, total_gas_used)
176+
177+
178+
@pytest.fixture
179+
def post(
180+
gas_measure_contract: Address,
181+
call_contract_post_storage: Storage,
182+
) -> Dict[Address, Account]:
183+
"""Return expected post state with gas consumption check."""
184+
return {
185+
gas_measure_contract: Account(storage=call_contract_post_storage),
186+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
precompile/test/call_contexts/set_code = Covered in EIP-7702 cases
2+
precompile/test/call_contexts/normal = Covered in osaka/eip7883_modexp_gas_increase
3+
precompile/test/call_contexts/delegate = Covered in osaka/eip7883_modexp_gas_increase
4+
precompile/test/call_contexts/static = Covered in osaka/eip7883_modexp_gas_increase
5+
precompile/test/call_contexts/callcode = Covered in osaka/eip7883_modexp_gas_increase
6+
precompile/test/call_contexts/tx_entry = Covered in osaka/eip7883_modexp_gas_increase
7+
precompile/test/call_contexts/initcode/CREATE = Covered in osaka/eip7883_modexp_gas_increase
8+
precompile/test/call_contexts/initcode/tx = Covered in osaka/eip7883_modexp_gas_increase
9+
precompile/test/call_contexts/set_code = Covered in osaka/eip7883_modexp_gas_increase
10+
precompile/test/inputs/valid = Covered in osaka/eip7883_modexp_gas_increase
11+
precompile/test/inputs/valid/boundary = Covered in osaka/eip7883_modexp_gas_increase
12+
precompile/test/inputs/all_zeros = Covered in osaka/eip7883_modexp_gas_increase
13+
precompile/test/inputs/invalid = Covered in osaka/eip7883_modexp_gas_increase
14+
precompile/test/inputs/invalid/crypto = Covered in osaka/eip7883_modexp_gas_increase
15+
precompile/test/inputs/invalid/corrupted = Covered in osaka/eip7883_modexp_gas_increase
16+
precompile/test/value_transfer/no_fee = Covered in osaka/eip7883_modexp_gas_increase
17+
precompile/test/out_of_bounds/max_plus_one = Covered in osaka/eip7883_modexp_gas_increase
18+
precompile/test/input_lengths/zero = Covered in osaka/eip7883_modexp_gas_increase
19+
precompile/test/input_lengths/dynamic/valid = Covered in osaka/eip7883_modexp_gas_increase
20+
precompile/test/input_lengths/dynamic/too_long = Covered in osaka/eip7883_modexp_gas_increase
21+
precompile/test/gas_usage/dynamic/exact = Covered in osaka/eip7883_modexp_gas_increase
22+
precompile/test/gas_usage/dynamic/oog = Covered in osaka/eip7883_modexp_gas_increase
23+
precompile/test/excessive_gas_usage = Covered in osaka/eip7883_modexp_gas_increase
24+
precompile/test/fork_transition/after/warm = Covered in osaka/eip7883_modexp_gas_increase
25+
gas_cost_changes/test/gas_updates_measurement = Covered in osaka/eip7883_modexp_gas_increase
26+
gas_cost_changes/test/fork_transition/before = Covered in osaka/eip7883_modexp_gas_increase
27+
gas_cost_changes/test/fork_transition/after = Covered in osaka/eip7883_modexp_gas_increase
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
precompile/test/value_transfer/fee/under = No value is required
2+
precompile/test/value_transfer/fee/exact = No value is required
3+
precompile/test/value_transfer/fee/over = No value is required
4+
precompile/test/input_lengths/static/correct = The Modexp input length is not static
5+
precompile/test/input_lengths/static/too_short = The Modexp input length is not static
6+
precompile/test/input_lengths/static/too_long = The Modexp input length is not static
7+
precompile/test/input_lengths/dynamic/too_short = there would be no padding for precompile
8+
precompile/test/gas_usage/constant/oog = The Modexp gas cost is dynamic
9+
precompile/test/gas_usage/constant/exact = The Modexp gas cost is dynamic
10+
precompile/test/fork_transition/before/invalid_input = Modexp is not new precompile, it is still valid befork fork activation
11+
precompile/test/fork_transition/before/zero_gas = Modexp is not new precompile, it is still valid befork fork activation
12+
precompile/test/fork_transition/before/cold = Modexp is not new precompile, it is still valid befork fork activation
13+
gas_cost_changes/test/out_of_gas = No Out-of-gas scenario in Modexp
14+
system_contract = EIP does not include a new system contract
15+
opcode = EIP does not introduce a new opcode
16+
removed_precompile = EIP does not remove a precompile
17+
transaction_type = EIP does not introduce a new transaction type
18+
block_header_field = EIP does not add any new block header fields
19+
gas_refunds_changes = EIP does not introduce any gas refund changes
20+
blob_count_changes = EIP does not introduce any blob count changes
21+
execution_layer_request = EIP does not introduce an execution layer request
22+
new_transaction_validity_constraint = EIP does not introduce a new transaction validity constraint
23+
modified_transaction_validity_constraint = EIP does not introduce a modified transaction validity constraint
24+
block_body_field = EIP does not add any new block body fields
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Defines EIP-7823 specification constants and functions."""
2+
3+
from dataclasses import dataclass
4+
5+
6+
@dataclass(frozen=True)
7+
class ReferenceSpec:
8+
"""Defines the reference spec version and git path."""
9+
10+
git_path: str
11+
version: str
12+
13+
14+
ref_spec_7823 = ReferenceSpec("EIPS/eip-7823.md", "c8321494fdfbfda52ad46c3515a7ca5dc86b857c")

0 commit comments

Comments
 (0)