Skip to content

Commit 016bf91

Browse files
refactor(eip7883): update vector input structure
1 parent 0fdd974 commit 016bf91

File tree

4 files changed

+145
-74
lines changed

4 files changed

+145
-74
lines changed

tests/osaka/eip7883_modexp_gas_increase/conftest.py

Lines changed: 105 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ethereum_test_tools import Account, Address, Alloc, Storage, Transaction
99
from ethereum_test_tools.vm.opcode import Opcodes as Op
1010

11-
from .helpers import Vector
11+
from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput
1212
from .spec import Spec, Spec7883
1313

1414

@@ -19,97 +19,148 @@ def call_opcode() -> Op:
1919

2020

2121
@pytest.fixture
22-
def gas_measure_contract(pre: Alloc, call_opcode: Op, fork: Fork, vector: Vector) -> Address:
22+
def call_contract_post_storage() -> Storage:
23+
"""
24+
Storage of the test contract after the transaction is executed.
25+
Note: Fixture `call_contract_code` fills the actual expected storage values.
26+
"""
27+
return Storage()
28+
29+
30+
@pytest.fixture
31+
def gas_measure_contract(
32+
pre: Alloc,
33+
call_opcode: Op,
34+
fork: Fork,
35+
modexp_expected: bytes,
36+
precompile_gas: int,
37+
precompile_gas_modifier: int,
38+
call_contract_post_storage: Storage,
39+
) -> Address:
2340
"""Deploys a contract that measures ModExp gas consumption."""
41+
assert call_opcode in [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL]
42+
value = [0] if call_opcode in [Op.CALL, Op.CALLCODE] else []
43+
2444
call_code = call_opcode(
25-
address=Spec.MODEXP_ADDRESS,
26-
value=0,
27-
args_offset=0,
28-
args_size=Op.CALLDATASIZE,
45+
precompile_gas + precompile_gas_modifier,
46+
Spec.MODEXP_ADDRESS,
47+
*value,
48+
0,
49+
Op.CALLDATASIZE(),
50+
0,
51+
0,
2952
)
53+
3054
gas_costs = fork.gas_costs()
3155
extra_gas = (
3256
gas_costs.G_WARM_ACCOUNT_ACCESS
33-
+ (gas_costs.G_VERY_LOW * (len(call_opcode.kwargs) - 2)) # type: ignore
34-
+ (gas_costs.G_BASE * 3)
57+
+ (gas_costs.G_VERY_LOW * (len(call_opcode.kwargs) - 1)) # type: ignore
58+
+ gas_costs.G_BASE # CALLDATASIZE
59+
+ gas_costs.G_BASE # GAS
3560
)
36-
measure_code = (
61+
62+
# Build the gas measurement contract code
63+
# Stack operations:
64+
# [gas_start]
65+
# [gas_start, call_result]
66+
# [gas_start, call_result, gas_end]
67+
# [gas_start, gas_end, call_result]
68+
call_result_measurement = Op.GAS + call_code + Op.GAS + Op.SWAP1
69+
70+
# Calculate gas consumed: gas_start - (gas_end + extra_gas)
71+
# Stack Operation:
72+
# [gas_start, gas_end]
73+
# [gas_start, gas_end, extra_gas]
74+
# [gas_start, gas_end + extra_gas]
75+
# [gas_end + extra_gas, gas_start]
76+
# [gas_consumed]
77+
gas_calculation = Op.PUSH2[extra_gas] + Op.ADD + Op.SWAP1 + Op.SUB
78+
79+
code = (
3780
Op.CALLDATACOPY(dest_offset=0, offset=0, size=Op.CALLDATASIZE)
38-
+ Op.GAS # [gas_start]
39-
+ call_code # [gas_start, call_result]
40-
+ Op.GAS # [gas_start, call_result, gas_end]
41-
+ Op.SWAP1 # [gas_start, gas_end, call_result]
42-
+ Op.PUSH1[0] # [gas_start, gas_end, call_result, 0]
43-
+ Op.SSTORE # [gas_start, gas_end]
44-
+ Op.PUSH2[extra_gas] # [gas_start, gas_end, extra_gas]
45-
+ Op.ADD # [gas_start, gas_end + extra_gas]
46-
+ Op.SWAP1 # [gas_end + extra_gas, gas_start]
47-
+ Op.SUB # [gas_start - (gas_end + extra_gas)]
48-
+ Op.PUSH1[1] # [gas_start - (gas_end + extra_gas), 1]
49-
+ Op.SSTORE # []
81+
+ Op.SSTORE(call_contract_post_storage.store_next(True), call_result_measurement)
82+
+ Op.SSTORE(call_contract_post_storage.store_next(precompile_gas), gas_calculation)
83+
+ Op.SSTORE(
84+
call_contract_post_storage.store_next(len(modexp_expected)),
85+
Op.RETURNDATASIZE(),
86+
)
87+
# + Op.RETURNDATACOPY(dest_offset=0, offset=0, size=Op.RETURNDATASIZE())
88+
# + Op.SSTORE(call_contract_post_storage.store_next(
89+
# keccak256(Bytes(vector.expected))), Op.SHA3(0, Op.RETURNDATASIZE()))
5090
)
51-
measure_code += Op.SSTORE(2, Op.RETURNDATASIZE())
52-
for i in range(len(vector.expected) // 32):
53-
measure_code += Op.RETURNDATACOPY(0, i * 32, 32)
54-
measure_code += Op.SSTORE(i + 3, Op.MLOAD(0))
55-
measure_code += Op.STOP()
56-
return pre.deploy_contract(measure_code)
91+
for i in range(len(modexp_expected) // 32):
92+
code += Op.RETURNDATACOPY(0, i * 32, 32)
93+
code += Op.SSTORE(
94+
call_contract_post_storage.store_next(modexp_expected[i * 32 : (i + 1) * 32]),
95+
Op.MLOAD(0),
96+
)
97+
98+
return pre.deploy_contract(code)
5799

58100

59101
@pytest.fixture
60-
def precompile_gas(fork: Fork, vector: Vector) -> int:
102+
def precompile_gas(fork: Fork, modexp_input: ModExpInput, gas_old: int, gas_new: int) -> int:
61103
"""Calculate gas cost for the ModExp precompile and verify it matches expected gas."""
62104
spec = Spec if fork < Osaka else Spec7883
63-
expected_gas = vector.gas_old if fork < Osaka else vector.gas_new
105+
expected_gas = gas_old if fork < Osaka else gas_new
64106
calculated_gas = spec.calculate_gas_cost(
65-
len(vector.input.base),
66-
len(vector.input.modulus),
67-
len(vector.input.exponent),
68-
vector.input.exponent,
107+
len(modexp_input.base),
108+
len(modexp_input.modulus),
109+
len(modexp_input.exponent),
110+
modexp_input.exponent,
69111
)
70112
assert calculated_gas == expected_gas, (
71113
f"Calculated gas {calculated_gas} != Vector gas {expected_gas}"
72114
)
73115
return calculated_gas
74116

75117

118+
@pytest.fixture
119+
def precompile_gas_modifier() -> int:
120+
"""Return the gas modifier for the ModExp precompile."""
121+
return 0
122+
123+
76124
@pytest.fixture
77125
def tx(
78-
fork: Fork,
79126
pre: Alloc,
80127
gas_measure_contract: Address,
81-
vector: Vector,
82-
precompile_gas: int,
128+
modexp_input: ModExpInput,
129+
tx_gas_limit: int,
83130
) -> Transaction:
84131
"""Transaction to measure gas consumption of the ModExp precompile."""
85-
intrinsic_gas_cost_calc = fork.transaction_intrinsic_cost_calculator()
86-
intrinsic_gas_cost = intrinsic_gas_cost_calc(calldata=vector.input)
87-
memory_expansion_gas_calc = fork.memory_expansion_gas_calculator()
88-
memory_expansion_gas = memory_expansion_gas_calc(new_bytes=len(bytes(vector.input)))
89-
sstore_gas = fork.gas_costs().G_STORAGE_SET * (len(vector.expected) // 32)
90132
return Transaction(
91133
sender=pre.fund_eoa(),
92134
to=gas_measure_contract,
93-
data=vector.input,
94-
gas_limit=intrinsic_gas_cost
135+
data=bytes(modexp_input),
136+
gas_limit=tx_gas_limit,
137+
)
138+
139+
140+
@pytest.fixture
141+
def tx_gas_limit(
142+
fork: Fork, modexp_expected: bytes, modexp_input: ModExpInput, precompile_gas: int
143+
) -> int:
144+
"""Transaction gas limit used for the test (Can be overridden in the test)."""
145+
intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
146+
memory_expansion_gas_calculator = fork.memory_expansion_gas_calculator()
147+
sstore_gas = fork.gas_costs().G_STORAGE_SET * (len(modexp_expected) // 32)
148+
extra_gas = 100_000
149+
return (
150+
extra_gas
151+
+ intrinsic_gas_cost_calculator(calldata=bytes(modexp_input))
152+
+ memory_expansion_gas_calculator(new_bytes=len(bytes(modexp_input)))
95153
+ precompile_gas
96-
+ memory_expansion_gas
97154
+ sstore_gas
98-
+ 100_000,
99155
)
100156

101157

102158
@pytest.fixture
103159
def post(
104160
gas_measure_contract: Address,
105-
precompile_gas: int,
106-
vector: Vector,
161+
call_contract_post_storage: Storage,
107162
) -> Dict[Address, Account]:
108163
"""Return expected post state with gas consumption check."""
109-
storage = Storage()
110-
storage[0] = 1
111-
storage[1] = precompile_gas
112-
storage[2] = len(vector.expected)
113-
for i in range(len(vector.expected) // 32):
114-
storage[i + 3] = vector.expected[i * 32 : (i + 1) * 32]
115-
return {gas_measure_contract: Account(storage=storage)}
164+
return {
165+
gas_measure_contract: Account(storage=call_contract_post_storage),
166+
}
Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,57 @@
11
"""Helper functions for the EIP-7883 ModExp gas cost increase tests."""
22

3-
import json
43
import os
54
from typing import Annotated, List
65

7-
from pydantic import BaseModel, Field, PlainValidator
6+
import pytest
7+
from pydantic import BaseModel, ConfigDict, Field, PlainValidator, RootModel, TypeAdapter
8+
from pydantic.alias_generators import to_pascal
89

910
from ethereum_test_tools import Bytes
1011

1112
from ...byzantium.eip198_modexp_precompile.helpers import ModExpInput
1213

1314

15+
def current_python_script_directory(*args: str) -> str:
16+
"""Get the current Python script directory."""
17+
return os.path.join(os.path.dirname(os.path.realpath(__file__)), *args)
18+
19+
1420
class Vector(BaseModel):
1521
"""A vector for the ModExp gas cost increase tests."""
1622

17-
input: Annotated[ModExpInput, PlainValidator(ModExpInput.from_bytes)] = Field(
23+
modexp_input: Annotated[ModExpInput, PlainValidator(ModExpInput.from_bytes)] = Field(
1824
..., alias="Input"
1925
)
20-
expected: Bytes = Field(..., alias="Expected")
26+
modexp_expected: Bytes = Field(..., alias="Expected")
2127
name: str = Field(..., alias="Name")
2228
gas_old: int | None = Field(..., alias="GasOld")
2329
gas_new: int | None = Field(..., alias="GasNew")
2430

25-
@staticmethod
26-
def from_json(vector_json: dict) -> "Vector":
27-
"""Create a Vector from a JSON dictionary."""
28-
return Vector.model_validate(vector_json)
31+
model_config = ConfigDict(alias_generator=to_pascal)
2932

30-
@staticmethod
31-
def from_file(filename: str) -> List["Vector"]:
32-
"""Create a list of Vectors from a file."""
33-
with open(current_python_script_directory(filename), "r") as f:
34-
vectors_json = json.load(f)
35-
return [Vector.from_json(vector_json) for vector_json in vectors_json]
33+
def to_pytest_param(self):
34+
"""Convert the test vector to a tuple that can be used as a parameter in a pytest test."""
35+
return pytest.param(
36+
self.modexp_input, self.modexp_expected, self.gas_old, self.gas_new, id=self.name
37+
)
3638

3739

38-
def current_python_script_directory(*args: str) -> str:
39-
"""Get the current Python script directory."""
40-
return os.path.join(os.path.dirname(os.path.realpath(__file__)), *args)
40+
class VectorList(RootModel):
41+
"""A list of test vectors for the ModExp gas cost increase tests."""
42+
43+
root: List[Vector]
44+
45+
46+
VectorListAdapter = TypeAdapter(VectorList)
47+
48+
49+
def vectors_from_file(filename: str) -> List:
50+
"""Load test vectors from a file."""
51+
with open(
52+
current_python_script_directory(
53+
filename,
54+
),
55+
"rb",
56+
) as f:
57+
return [v.to_pytest_param() for v in VectorListAdapter.validate_json(f.read()).root]

tests/osaka/eip7883_modexp_gas_increase/test_modexp_thresholds.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
Transaction,
1414
)
1515

16-
from .helpers import Vector
16+
from .helpers import vectors_from_file
1717
from .spec import ref_spec_7883
1818

1919
REFERENCE_SPEC_GIT_PATH = ref_spec_7883.git_path
@@ -22,9 +22,12 @@
2222
pytestmark = pytest.mark.valid_from("Prague")
2323

2424

25-
@pytest.mark.parametrize("vector", Vector.from_file("vectors.json"), ids=lambda v: v.name)
25+
@pytest.mark.parametrize(
26+
"modexp_input,modexp_expected,gas_old,gas_new",
27+
vectors_from_file("vectors.json"),
28+
ids=lambda v: v.name,
29+
)
2630
def test_vectors_from_file(
27-
vector: Vector,
2831
state_test: StateTestFiller,
2932
pre: Alloc,
3033
tx: Transaction,

0 commit comments

Comments
 (0)