Skip to content

Commit f6fb3cf

Browse files
committed
new(tests): EIP-7623: Add gas consumption tests
1 parent fda5ed3 commit f6fb3cf

File tree

4 files changed

+674
-304
lines changed

4 files changed

+674
-304
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
"""
2+
Fixtures for the EIP-7623 tests.
3+
"""
4+
5+
from typing import Callable, List, Sequence
6+
7+
import pytest
8+
9+
from ethereum_test_forks import Fork
10+
from ethereum_test_tools import (
11+
EOA,
12+
AccessList,
13+
Address,
14+
Alloc,
15+
AuthorizationTuple,
16+
Bytecode,
17+
Bytes,
18+
Hash,
19+
)
20+
from ethereum_test_tools import Opcodes as Op
21+
from ethereum_test_tools import Transaction, TransactionException
22+
23+
from .helpers import DataTestType
24+
25+
26+
@pytest.fixture
27+
def sender(pre: Alloc) -> EOA:
28+
"""
29+
Create the sender account.
30+
"""
31+
return pre.fund_eoa()
32+
33+
34+
@pytest.fixture
35+
def to(
36+
request: pytest.FixtureRequest,
37+
pre: Alloc,
38+
) -> Address | None:
39+
"""
40+
Create the sender account.
41+
"""
42+
if hasattr(request, "param"):
43+
param = request.param
44+
else:
45+
param = Op.STOP
46+
47+
if param is None:
48+
return None
49+
if isinstance(param, Address):
50+
return param
51+
if isinstance(param, Bytecode):
52+
return pre.deploy_contract(param)
53+
54+
raise ValueError(f"Invalid value for `to` fixture: {param}")
55+
56+
57+
@pytest.fixture
58+
def protected() -> bool:
59+
"""
60+
Whether the transaction is protected or not. Only valid for type-0 transactions.
61+
"""
62+
return True
63+
64+
65+
@pytest.fixture
66+
def access_list() -> List[AccessList] | None:
67+
"""
68+
Access list for the transaction.
69+
"""
70+
return None
71+
72+
73+
@pytest.fixture
74+
def authorization_existing_authority() -> bool:
75+
"""
76+
Whether the transaction has an existing authority in the authorization list.
77+
"""
78+
return False
79+
80+
81+
@pytest.fixture
82+
def authorization_list(
83+
request: pytest.FixtureRequest,
84+
pre: Alloc,
85+
authorization_existing_authority: bool,
86+
) -> List[AuthorizationTuple] | None:
87+
"""
88+
Authorization list for the transaction.
89+
90+
This fixture needs to be parametrized indirectly in order to generate the authorizations with
91+
valid signers using `pre` in this function, and the parametrized value should be a list of
92+
addresses.
93+
"""
94+
if not hasattr(request, "param"):
95+
return None
96+
if request.param is None:
97+
return None
98+
return [
99+
AuthorizationTuple(
100+
signer=pre.fund_eoa(1 if authorization_existing_authority else 0), address=address
101+
)
102+
for address in request.param
103+
]
104+
105+
106+
@pytest.fixture
107+
def blob_versioned_hashes() -> Sequence[Hash] | None:
108+
"""
109+
Versioned hashes for the transaction.
110+
"""
111+
return None
112+
113+
114+
@pytest.fixture
115+
def contract_creating_tx(to: Address | None) -> bool:
116+
"""
117+
Whether the transaction creates a contract or not.
118+
"""
119+
return to is None
120+
121+
122+
def floor_cost_find(
123+
floor_data_gas_cost_calculator: Callable[[int], int],
124+
intrinsic_gas_cost_calculator: Callable[[int], int],
125+
) -> int:
126+
"""
127+
Find the minimum amount of tokens that will trigger the floor gas cost, by using a binary
128+
search and the intrinsic gas cost and floor data calculators.
129+
"""
130+
# Start with 1000 tokens and if the intrinsic gas cost is greater than the floor gas cost,
131+
# multiply the number of tokens by 2 until it's not.
132+
tokens = 1000
133+
while floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens):
134+
tokens *= 2
135+
136+
# Binary search to find the minimum number of tokens that will trigger the floor gas cost.
137+
left = 0
138+
right = tokens
139+
while left < right:
140+
tokens = (left + right) // 2
141+
if floor_data_gas_cost_calculator(tokens) < intrinsic_gas_cost_calculator(tokens):
142+
left = tokens + 1
143+
else:
144+
right = tokens
145+
tokens = left
146+
147+
if floor_data_gas_cost_calculator(tokens) > intrinsic_gas_cost_calculator(tokens):
148+
tokens -= 1
149+
150+
# Verify that increasing the tokens by one would always trigger the floor gas cost.
151+
assert (
152+
floor_data_gas_cost_calculator(tokens) <= intrinsic_gas_cost_calculator(tokens)
153+
) and floor_data_gas_cost_calculator(tokens + 1) > intrinsic_gas_cost_calculator(
154+
tokens + 1
155+
), "invalid case"
156+
157+
return tokens
158+
159+
160+
@pytest.fixture
161+
def intrinsic_gas_data_floor_minimum_delta() -> int:
162+
"""
163+
Induce a minimum delta between the transaction intrinsic gas cost and the
164+
floor data gas cost.
165+
"""
166+
return 0
167+
168+
169+
@pytest.fixture
170+
def tx_data(
171+
fork: Fork,
172+
data_test_type: DataTestType,
173+
access_list: List[AccessList] | None,
174+
authorization_list: List[AuthorizationTuple] | None,
175+
contract_creating_tx: bool,
176+
intrinsic_gas_data_floor_minimum_delta: int,
177+
) -> Bytes:
178+
"""
179+
All tests in this file use data that is generated dynamically depending on the case and the
180+
attributes of the transaction in order to reach the edge cases where the floor gas cost is
181+
equal or barely greater than the intrinsic gas cost.
182+
183+
We have two different types of tests:
184+
- FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS: The floor gas cost is less than or equal
185+
to the intrinsic gas cost, which means that the size of the tokens in the data are not
186+
enough to trigger the floor gas cost.
187+
- FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS: The floor gas cost is greater than the intrinsic
188+
gas cost, which means that the size of the tokens in the data are enough to trigger the
189+
floor gas cost.
190+
191+
E.g. Given a transaction with a single access list and a single storage key, its intrinsic gas
192+
cost (as of Prague fork) can be calculated as:
193+
- 21,000 gas for the transaction
194+
- 2,400 gas for the access list
195+
- 1,900 gas for the storage key
196+
- 16 gas for each non-zero byte in the data
197+
- 4 gas for each zero byte in the data
198+
199+
Its floor data gas cost can be calculated as:
200+
- 21,000 gas for the transaction
201+
- 40 gas for each non-zero byte in the data
202+
- 10 gas for each zero byte in the data
203+
204+
Notice that the data included in the transaction affects both the intrinsic gas cost and the
205+
floor data cost, but at different rates.
206+
207+
The purpose of this function is to find the exact amount of data where the floor data gas
208+
cost starts exceeding the intrinsic gas cost.
209+
210+
After a binary search we find that adding 717 tokens of data (179 non-zero bytes +
211+
1 zero byte) triggers the floor gas cost.
212+
213+
Therefore, this function will return a Bytes object with 179 non-zero bytes and 1 zero byte
214+
for `FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS` and a Bytes object with 179 non-zero bytes
215+
and no zero bytes for `FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS`
216+
"""
217+
218+
def tokens_to_data(tokens: int) -> Bytes:
219+
return Bytes(b"\x01" * (tokens // 4) + b"\x00" * (tokens % 4))
220+
221+
fork_intrinsic_cost_calculator = fork.transaction_intrinsic_cost_calculator()
222+
223+
def transaction_intrinsic_cost_calculator(tokens: int) -> int:
224+
return (
225+
fork_intrinsic_cost_calculator(
226+
calldata=tokens_to_data(tokens),
227+
contract_creation=contract_creating_tx,
228+
access_list=access_list,
229+
authorization_list_or_count=authorization_list,
230+
return_cost_deducted_prior_execution=True,
231+
)
232+
+ intrinsic_gas_data_floor_minimum_delta
233+
)
234+
235+
fork_data_floor_cost_calculator = fork.transaction_data_floor_cost_calculator()
236+
237+
def transaction_data_floor_cost_calculator(tokens: int) -> int:
238+
return fork_data_floor_cost_calculator(data=tokens_to_data(tokens))
239+
240+
# Start with zero data and check the difference in the gas calculator between the
241+
# intrinsic gas cost and the floor gas cost.
242+
if transaction_data_floor_cost_calculator(0) >= transaction_intrinsic_cost_calculator(0):
243+
# Special case which is a transaction with no extra intrinsic gas costs other than the
244+
# data cost, any data will trigger the floor gas cost.
245+
if data_test_type == DataTestType.FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS:
246+
return Bytes(b"")
247+
else:
248+
return Bytes(b"\0")
249+
250+
tokens = floor_cost_find(
251+
floor_data_gas_cost_calculator=transaction_data_floor_cost_calculator,
252+
intrinsic_gas_cost_calculator=transaction_intrinsic_cost_calculator,
253+
)
254+
255+
if data_test_type == DataTestType.FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS:
256+
return tokens_to_data(tokens + 1)
257+
return tokens_to_data(tokens)
258+
259+
260+
@pytest.fixture
261+
def tx_gas_delta() -> int:
262+
"""
263+
Gas delta to modify the gas amount included with the transaction.
264+
265+
If negative, the transaction will be invalid because the intrinsic gas cost is greater than the
266+
gas limit.
267+
268+
This value operates regardless of whether the floor data gas cost is reached or not.
269+
270+
If the value is greater than zero, the transaction will also be valid and the test will check
271+
that transaction processing does not consume more gas than it should.
272+
"""
273+
return 0
274+
275+
276+
@pytest.fixture
277+
def tx_gas(
278+
fork: Fork,
279+
tx_data: Bytes,
280+
access_list: List[AccessList] | None,
281+
authorization_list: List[AuthorizationTuple] | None,
282+
contract_creating_tx: bool,
283+
tx_gas_delta: int,
284+
) -> int:
285+
"""
286+
Gas limit for the transaction.
287+
288+
The calculated value takes into account the normal intrinsic gas cost and the floor data gas
289+
cost.
290+
291+
The gas delta is added to the intrinsic gas cost to generate different test scenarios.
292+
"""
293+
intrinsic_gas_cost_calculator = fork.transaction_intrinsic_cost_calculator()
294+
return (
295+
intrinsic_gas_cost_calculator(
296+
calldata=tx_data,
297+
contract_creation=contract_creating_tx,
298+
access_list=access_list,
299+
authorization_list_or_count=authorization_list,
300+
)
301+
+ tx_gas_delta
302+
)
303+
304+
305+
@pytest.fixture
306+
def tx_error(tx_gas_delta: int) -> TransactionException | None:
307+
"""
308+
Transaction error, only expected if the gas delta is negative.
309+
"""
310+
return TransactionException.INTRINSIC_GAS_TOO_LOW if tx_gas_delta < 0 else None
311+
312+
313+
@pytest.fixture
314+
def tx(
315+
sender: EOA,
316+
ty: int,
317+
tx_data: Bytes,
318+
to: Address | None,
319+
protected: bool,
320+
access_list: List[AccessList] | None,
321+
authorization_list: List[AuthorizationTuple] | None,
322+
blob_versioned_hashes: Sequence[Hash] | None,
323+
tx_gas: int,
324+
tx_error: TransactionException | None,
325+
) -> Transaction:
326+
"""
327+
Create the transaction used in each test.
328+
"""
329+
return Transaction(
330+
ty=ty,
331+
sender=sender,
332+
data=tx_data,
333+
to=to,
334+
protected=protected,
335+
access_list=access_list,
336+
authorization_list=authorization_list,
337+
gas_limit=tx_gas,
338+
blob_versioned_hashes=blob_versioned_hashes,
339+
error=tx_error,
340+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Helpers for testing EIP-7623.
3+
"""
4+
5+
from enum import Enum, auto
6+
7+
8+
class DataTestType(Enum):
9+
"""
10+
Enum for the different types of data tests.
11+
"""
12+
13+
FLOOR_GAS_COST_LESS_THAN_OR_EQUAL_TO_INTRINSIC_GAS = auto()
14+
FLOOR_GAS_COST_GREATER_THAN_INTRINSIC_GAS = auto()
15+
16+
17+
class GasTestType(Enum):
18+
"""
19+
Enum for the different types of gas tests.
20+
"""
21+
22+
CONSUME_ZERO_GAS = auto()
23+
CONSUME_ALL_GAS = auto()
24+
CONSUME_ALL_GAS_WITH_REFUND = auto()

0 commit comments

Comments
 (0)