Skip to content

Commit df81019

Browse files
refactor(tests): add checklist marker for eip7825 (#2107)
* chore: add max quotient for gas refund logic * refactor: update the eip checklist items * feat: label eip checklist and add new cases * refactor: update fork transition case * fix typo * refactor: update external coverage * refactor: update ref spec and comment
1 parent 1779001 commit df81019

File tree

8 files changed

+157
-31
lines changed

8 files changed

+157
-31
lines changed

src/ethereum_test_checklists/eip_checklist.pyi

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Type stubs for EIP checklist - auto-generated.
33
4-
DO NOT EDIT MANUALLY - This file is generated by `uv run generate-checklist-stubs`
4+
DO NOT EDIT MANUALLY - This file is generated by `uv run generate_checklist_stubs`
55
"""
66

77
from typing import Any, Callable, TypeVar, overload
@@ -89,6 +89,21 @@ class EIPChecklist:
8989
SecondClient: _CallableChecklistItem
9090
TestCoverage: _CallableChecklistItem
9191

92+
class ModifiedTransactionValidityConstraint(_CallableChecklistItem):
93+
class Test(_CallableChecklistItem):
94+
class ForkTransition(_CallableChecklistItem):
95+
AcceptedAfterFork: _CallableChecklistItem
96+
AcceptedBeforeFork: _CallableChecklistItem
97+
RejectedAfterFork: _CallableChecklistItem
98+
RejectedBeforeFork: _CallableChecklistItem
99+
100+
class NewTransactionValidityConstraint(_CallableChecklistItem):
101+
class Test(_CallableChecklistItem):
102+
class ForkTransition(_CallableChecklistItem):
103+
AcceptedAfterFork: _CallableChecklistItem
104+
AcceptedBeforeFork: _CallableChecklistItem
105+
RejectedAfterFork: _CallableChecklistItem
106+
92107
class Opcode(_CallableChecklistItem):
93108
class Test(_CallableChecklistItem):
94109
ExceptionalAbort: _CallableChecklistItem

src/ethereum_test_forks/base_fork.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ def base_fee_elasticity_multiplier(cls, block_number: int = 0, timestamp: int =
333333
"""Return the base fee elasticity multiplier at a given fork."""
334334
pass
335335

336+
@classmethod
337+
@abstractmethod
338+
def max_refund_quotient(cls) -> int:
339+
"""Return the max refund quotient at a given fork."""
340+
pass
341+
336342
@classmethod
337343
@abstractmethod
338344
def transaction_data_floor_cost_calculator(

src/ethereum_test_forks/forks/forks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,11 @@ def create_opcodes(
605605
(Opcodes.CREATE, EVMCodeType.LEGACY),
606606
]
607607

608+
@classmethod
609+
def max_refund_quotient(cls) -> int:
610+
"""Return the max refund quotient at Genesis."""
611+
return 2
612+
608613
@classmethod
609614
def max_request_type(cls, block_number: int = 0, timestamp: int = 0) -> int:
610615
"""At genesis, no request type is supported, signaled by -1."""
@@ -913,6 +918,11 @@ def valid_opcodes(
913918
"""Return list of Opcodes that are valid to work on this fork."""
914919
return [Opcodes.BASEFEE] + super(London, cls).valid_opcodes()
915920

921+
@classmethod
922+
def max_refund_quotient(cls) -> int:
923+
"""Return the max refund quotient at London."""
924+
return 5
925+
916926
@classmethod
917927
def base_fee_max_change_denominator(cls, block_number: int = 0, timestamp: int = 0) -> int:
918928
"""Return the base fee max change denominator at London."""
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
general/code_coverage/eels = Please check https://app.codecov.io/gh/ethereum/execution-specs/pull/1388/blob/src/ethereum/osaka/vm/instructions/bitwise.py#L243 for relevant test coverage
2+
general/code_coverage/test_coverage = Please run the test with `--cov` flag for final coverage
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
system_contract = EIP does not introduce a new system contract
2+
opcode = EIP does not introduce a new opcode
3+
precompile = EIP does not introduce a new precompile
4+
removed_precompile = EIP does not remove a precompile
5+
transaction_type = EIP does not introduce a new transaction type
6+
block_header_field = EIP does not add any new block header fields
7+
block_body_field = EIP does not add any new block body fields
8+
gas_cost_changes = EIP does not modify existing gas costs, only introduces new opcode with fixed cost
9+
gas_refunds_changes = EIP does not introduce any gas refund changes
10+
blob_count_changes = EIP does not introduce any blob count changes
11+
execution_layer_request = EIP does not introduce an execution layer request
12+
new_transaction_validity_constraint = EIP does not introduce a new transaction validity constraint

tests/osaka/eip7825_transaction_gas_limit_cap/spec.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class ReferenceSpec:
1212

1313

1414
# EIP-7825 reference specification
15-
ref_spec_7825 = ReferenceSpec("EIPS/eip-7825.md", "47cbfed315988c0bd4d10002c110ae402504cd94")
15+
ref_spec_7825 = ReferenceSpec("EIPS/eip-7825.md", "1ed95cbac750539c2aac67c8cbbcc2d77974231c")
1616

1717

1818
@dataclass(frozen=True)

tests/osaka/eip7825_transaction_gas_limit_cap/test_tx_gas_limit.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
AuthorizationTuple,
1717
Block,
1818
BlockchainTestFiller,
19+
Bytecode,
1920
Environment,
2021
Hash,
2122
StateTestFiller,
@@ -53,7 +54,7 @@ def tx_gas_limit_cap_tests(fork: Fork) -> List[ParameterSet]:
5354
id="tx_gas_limit_cap_exceeds_maximum",
5455
marks=pytest.mark.exception_test,
5556
),
56-
pytest.param(fork_tx_gas_limit_cap, None, id="tx_gas_limit_cap_none"),
57+
pytest.param(fork_tx_gas_limit_cap, None, id="tx_gas_limit_cap_over"),
5758
]
5859

5960

@@ -201,6 +202,68 @@ def test_tx_gas_larger_than_block_gas_limit(
201202
blockchain_test(pre=pre, post={}, blocks=[block])
202203

203204

205+
@pytest.mark.parametrize(
206+
"exceed_gas_refund_limit",
207+
[
208+
pytest.param(True),
209+
pytest.param(False),
210+
],
211+
)
212+
@pytest.mark.valid_from("Osaka")
213+
def test_maximum_gas_refund(
214+
state_test: StateTestFiller,
215+
pre: Alloc,
216+
fork: Fork,
217+
exceed_gas_refund_limit: bool,
218+
):
219+
"""Test the maximum gas refund behavior according to EIP-3529."""
220+
gas_costs = fork.gas_costs()
221+
tx_gas_limit_cap = fork.transaction_gas_limit_cap()
222+
assert tx_gas_limit_cap is not None, "Fork does not have a transaction gas limit cap"
223+
max_refund_quotient = fork.max_refund_quotient()
224+
225+
storage = Storage()
226+
227+
# Base Operation: SSTORE(slot, 0)
228+
iteration_cost = gas_costs.G_STORAGE_RESET + gas_costs.G_BASE + gas_costs.G_VERY_LOW
229+
gas_refund = gas_costs.R_STORAGE_CLEAR
230+
231+
# EIP-3529: Reduction in refunds
232+
storage_count = tx_gas_limit_cap // iteration_cost
233+
gas_used = storage_count * iteration_cost
234+
235+
maximum_gas_refund = gas_used // max_refund_quotient
236+
gas_refund_count = maximum_gas_refund // gas_refund
237+
238+
# Base case: operations that fit within the refund limit
239+
iteration_count = min(storage_count, gas_refund_count + int(exceed_gas_refund_limit))
240+
241+
assert iteration_cost * iteration_count <= tx_gas_limit_cap, (
242+
"Iteration cost exceeds tx gas limit cap"
243+
)
244+
245+
opcode = sum(
246+
(Op.SSTORE(storage.store_next(0), Op.PUSH0) for _ in range(iteration_count)),
247+
Bytecode(),
248+
)
249+
assert len(opcode) <= fork.max_code_size(), "code size exceeds max code size"
250+
251+
contract = pre.deploy_contract(
252+
code=opcode,
253+
storage={Hash(i): Hash(1) for i in range(iteration_count)},
254+
)
255+
256+
tx = Transaction(
257+
to=contract,
258+
sender=pre.fund_eoa(),
259+
gas_limit=tx_gas_limit_cap,
260+
)
261+
262+
post = {contract: Account(storage=storage)}
263+
264+
state_test(pre=pre, post=post, tx=tx)
265+
266+
204267
@pytest.fixture
205268
def total_cost_floor_per_token(fork: Fork):
206269
"""Total cost floor per token."""

tests/osaka/eip7825_transaction_gas_limit_cap/test_tx_gas_limit_transition_fork.py

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pytest
77

8+
from ethereum_test_checklists import EIPChecklist
89
from ethereum_test_forks import Fork
910
from ethereum_test_tools import (
1011
Account,
@@ -22,81 +23,98 @@
2223
REFERENCE_SPEC_VERSION = ref_spec_7825.version
2324

2425

26+
@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.AcceptedBeforeFork()
27+
@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.RejectedBeforeFork()
28+
@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.AcceptedAfterFork()
29+
@EIPChecklist.ModifiedTransactionValidityConstraint.Test.ForkTransition.RejectedAfterFork()
2530
@pytest.mark.valid_at_transition_to("Osaka", subsequent_forks=True)
26-
@pytest.mark.exception_test
31+
@pytest.mark.parametrize(
32+
"transaction_at_cap",
33+
[
34+
pytest.param(True, id="at_cap"),
35+
pytest.param(False, marks=pytest.mark.exception_test, id="above_cap"),
36+
],
37+
)
2738
def test_transaction_gas_limit_cap_at_transition(
2839
blockchain_test: BlockchainTestFiller,
2940
pre: Alloc,
3041
fork: Fork,
42+
transaction_at_cap: bool,
3143
):
3244
"""
3345
Test transaction gas limit cap behavior at the Osaka transition.
3446
3547
Before timestamp 15000: No gas limit cap (transactions with gas > 2^24 are valid)
3648
At/after timestamp 15000: Gas limit cap of 2^24 is enforced
3749
"""
38-
sender = pre.fund_eoa()
3950
contract_address = pre.deploy_contract(
40-
code=Op.SSTORE(0, Op.ADD(Op.SLOAD(0), 1)) + Op.STOP,
51+
code=Op.SSTORE(Op.TIMESTAMP, Op.ADD(Op.SLOAD(Op.TIMESTAMP), 1)) + Op.STOP,
4152
)
4253

43-
pre_cap = fork.transaction_gas_limit_cap()
44-
post_cap = fork.transaction_gas_limit_cap(timestamp=15_000)
45-
assert post_cap is not None, "Post cap should not be None"
54+
# Get the gas limit cap at fork activation
55+
tx_gas_cap = fork.transaction_gas_limit_cap(timestamp=15_000)
56+
assert tx_gas_cap is not None, "Gas limit cap should not be None after fork activation"
4657

47-
pre_cap = pre_cap if pre_cap else post_cap + 1
58+
# Test boundary: cap + 1 should fail after fork activation
59+
above_cap = tx_gas_cap + 1
4860

49-
assert post_cap <= pre_cap, (
50-
"Post cap should be less than or equal to pre cap, test needs update"
61+
# Before fork activation: both cap and above_cap transactions should succeed
62+
at_cap_tx_before_fork = Transaction(
63+
ty=0, # Legacy transaction
64+
to=contract_address,
65+
gas_limit=tx_gas_cap,
66+
sender=pre.fund_eoa(),
5167
)
5268

53-
# Transaction with gas limit above the cap before transition
54-
high_gas_tx = Transaction(
69+
above_cap_tx_before_fork = Transaction(
5570
ty=0, # Legacy transaction
5671
to=contract_address,
57-
gas_limit=pre_cap,
58-
data=b"",
59-
value=0,
60-
sender=sender,
72+
gas_limit=above_cap,
73+
sender=pre.fund_eoa(),
6174
)
6275

6376
post_cap_tx_error = TransactionException.GAS_LIMIT_EXCEEDS_MAXIMUM
6477

65-
# Transaction with gas limit at the cap
66-
cap_gas_tx = Transaction(
78+
# After fork activation: test at cap vs above cap
79+
transition_tx = Transaction(
6780
ty=0, # Legacy transaction
6881
to=contract_address,
69-
gas_limit=post_cap + 1,
70-
data=b"",
71-
value=0,
72-
sender=sender,
73-
error=post_cap_tx_error,
82+
gas_limit=tx_gas_cap if transaction_at_cap else above_cap,
83+
sender=pre.fund_eoa(),
84+
error=None if transaction_at_cap else post_cap_tx_error,
7485
)
7586

7687
blocks = []
7788

78-
# Before transition (timestamp < 15000): high gas transaction should succeed
89+
# Before transition (timestamp < 15000): both cap and above_cap transactions should succeed
7990
blocks.append(
8091
Block(
8192
timestamp=14_999,
82-
txs=[high_gas_tx],
93+
txs=[above_cap_tx_before_fork, at_cap_tx_before_fork],
8394
)
8495
)
8596

86-
# At transition (timestamp = 15000): high gas transaction should fail
97+
# At transition (timestamp = 15000):
98+
# - transaction at cap should succeed
99+
# - transaction above cap (cap + 1) should fail
87100
blocks.append(
88101
Block(
89102
timestamp=15_000,
90-
txs=[cap_gas_tx], # Only transaction at the cap succeeds
91-
exception=post_cap_tx_error,
103+
txs=[transition_tx],
104+
exception=post_cap_tx_error if not transaction_at_cap else None,
92105
)
93106
)
94107

95108
# Post state: storage should be updated by successful transactions
96109
post = {
97110
contract_address: Account(
98111
storage={
99-
0: 1, # Set by first transaction (before transition)
112+
# Set by both transactions in first block (before transition):
113+
14_999: 2,
114+
# After transition:
115+
# - Set by transaction at cap (should succeed)
116+
# - Not set by transaction above cap (should fail)
117+
15_000: 1 if transaction_at_cap else 0,
100118
}
101119
)
102120
}

0 commit comments

Comments
 (0)