Skip to content

Commit d7def91

Browse files
authored
feat(tests): add EIP-8024 gas boundary and SWAPN invalid-immediate tests (#2942)
1 parent abf6ff6 commit d7def91

3 files changed

Lines changed: 198 additions & 3 deletions

File tree

tests/amsterdam/eip8024_dupn_swapn_exchange/test_dupn.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
Account,
1111
Alloc,
1212
Bytecode,
13+
EIPChecklist,
14+
Fork,
1315
Op,
1416
StateTestFiller,
1517
Transaction,
1618
)
1719

18-
from .spec import decode_single, ref_spec_8024
20+
from .spec import Spec, decode_single, ref_spec_8024
1921

2022
REFERENCE_SPEC_GIT_PATH = ref_spec_8024.git_path
2123
REFERENCE_SPEC_VERSION = ref_spec_8024.version
@@ -142,6 +144,52 @@ def test_dupn_stack_underflow(
142144
state_test(pre=pre, post=post, tx=tx)
143145

144146

147+
@EIPChecklist.Opcode.Test.GasUsage.Normal()
148+
@EIPChecklist.Opcode.Test.GasUsage.OutOfGasExecution()
149+
@EIPChecklist.Opcode.Test.GasUsage.ExtraGas()
150+
@pytest.mark.parametrize("gas_cost_delta", [-2, -1, 0, 1, 2])
151+
def test_dupn_gas_cost_boundary(
152+
gas_cost_delta: int,
153+
pre: Alloc,
154+
fork: Fork,
155+
state_test: StateTestFiller,
156+
) -> None:
157+
"""
158+
Test DUPN at the gas cost boundary.
159+
160+
DUPN is invoked in a callee that receives exactly its execution cost
161+
plus `gas_cost_delta`. The caller records the CALL result: a negative
162+
delta starves DUPN of its base gas (3) and the sub-call runs out of
163+
gas (result 0); a zero or positive delta succeeds (result 1).
164+
"""
165+
stack_index = Spec.MIN_STACK_INDEX # 17
166+
167+
code = Bytecode()
168+
for i in range(stack_index):
169+
code += Op.PUSH1(i)
170+
code += Op.DUPN[stack_index]
171+
172+
contract_address = pre.deploy_contract(code=code)
173+
174+
call_code = Op.SSTORE(
175+
0,
176+
Op.CALL(
177+
gas=code.gas_cost(fork) + gas_cost_delta,
178+
address=contract_address,
179+
),
180+
)
181+
call_address = pre.deploy_contract(
182+
code=call_code,
183+
storage={0: 0xDEADBEEF},
184+
)
185+
186+
tx = Transaction(to=call_address, sender=pre.fund_eoa(), gas_limit=200_000)
187+
188+
post = {call_address: Account(storage={0: 0 if gas_cost_delta < 0 else 1})}
189+
190+
state_test(pre=pre, post=post, tx=tx)
191+
192+
145193
def test_endofcode_behavior(
146194
pre: Alloc,
147195
state_test: StateTestFiller,

tests/amsterdam/eip8024_dupn_swapn_exchange/test_exchange.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
Account,
1111
Alloc,
1212
Bytecode,
13+
EIPChecklist,
1314
Fork,
1415
Op,
1516
StateTestFiller,
1617
Transaction,
1718
)
1819

19-
from .spec import decode_pair, ref_spec_8024
20+
from .spec import Spec, decode_pair, ref_spec_8024
2021

2122
REFERENCE_SPEC_GIT_PATH = ref_spec_8024.git_path
2223
REFERENCE_SPEC_VERSION = ref_spec_8024.version
@@ -253,6 +254,57 @@ def test_exchange_stack_underflow(
253254
state_test(pre=pre, post=post, tx=tx)
254255

255256

257+
@EIPChecklist.Opcode.Test.GasUsage.Normal()
258+
@EIPChecklist.Opcode.Test.GasUsage.OutOfGasExecution()
259+
@EIPChecklist.Opcode.Test.GasUsage.ExtraGas()
260+
@pytest.mark.parametrize("gas_cost_delta", [-2, -1, 0, 1, 2])
261+
def test_exchange_gas_cost_boundary(
262+
gas_cost_delta: int,
263+
pre: Alloc,
264+
fork: Fork,
265+
state_test: StateTestFiller,
266+
) -> None:
267+
"""
268+
Test EXCHANGE at the gas cost boundary.
269+
270+
EXCHANGE is invoked in a callee that receives exactly its execution
271+
cost plus `gas_cost_delta`. The caller records the CALL result: a
272+
negative delta starves EXCHANGE of its base gas (3) and the sub-call
273+
runs out of gas (result 0); a zero or positive delta succeeds
274+
(result 1).
275+
"""
276+
# EXCHANGE with decoded (n, m) swaps position (n+1) with position
277+
# (m+1); since n < m it needs m + 1 items on the stack. Use the
278+
# smallest valid pair.
279+
n = Spec.EXCHANGE_MIN_N # 1
280+
m = n + 1
281+
282+
code = Bytecode()
283+
for i in range(m + 1):
284+
code += Op.PUSH1(i)
285+
code += Op.EXCHANGE[n, m]
286+
287+
contract_address = pre.deploy_contract(code=code)
288+
289+
call_code = Op.SSTORE(
290+
0,
291+
Op.CALL(
292+
gas=code.gas_cost(fork) + gas_cost_delta,
293+
address=contract_address,
294+
),
295+
)
296+
call_address = pre.deploy_contract(
297+
code=call_code,
298+
storage={0: 0xDEADBEEF},
299+
)
300+
301+
tx = Transaction(to=call_address, sender=pre.fund_eoa(), gas_limit=200_000)
302+
303+
post = {call_address: Account(storage={0: 0 if gas_cost_delta < 0 else 1})}
304+
305+
state_test(pre=pre, post=post, tx=tx)
306+
307+
256308
def test_endofcode_behavior(
257309
pre: Alloc,
258310
state_test: StateTestFiller,

tests/amsterdam/eip8024_dupn_swapn_exchange/test_swapn.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
Account,
1111
Alloc,
1212
Bytecode,
13+
EIPChecklist,
14+
Fork,
1315
Op,
1416
StateTestFiller,
1517
Transaction,
1618
)
1719

18-
from .spec import decode_single, ref_spec_8024
20+
from .spec import Spec, decode_single, ref_spec_8024
1921

2022
REFERENCE_SPEC_GIT_PATH = ref_spec_8024.git_path
2123
REFERENCE_SPEC_VERSION = ref_spec_8024.version
@@ -214,6 +216,99 @@ def test_swapn_stack_underflow(
214216
state_test(pre=pre, post=post, tx=tx)
215217

216218

219+
@EIPChecklist.Opcode.Test.GasUsage.Normal()
220+
@EIPChecklist.Opcode.Test.GasUsage.OutOfGasExecution()
221+
@EIPChecklist.Opcode.Test.GasUsage.ExtraGas()
222+
@pytest.mark.parametrize("gas_cost_delta", [-2, -1, 0, 1, 2])
223+
def test_swapn_gas_cost_boundary(
224+
gas_cost_delta: int,
225+
pre: Alloc,
226+
fork: Fork,
227+
state_test: StateTestFiller,
228+
) -> None:
229+
"""
230+
Test SWAPN at the gas cost boundary.
231+
232+
SWAPN is invoked in a callee that receives exactly its execution cost
233+
plus `gas_cost_delta`. The caller records the CALL result: a negative
234+
delta starves SWAPN of its base gas (3) and the sub-call runs out of
235+
gas (result 0); a zero or positive delta succeeds (result 1).
236+
"""
237+
# SWAPN with decoded value n swaps position 1 with position (n+1), so
238+
# it needs stack_index + 1 items on the stack.
239+
stack_index = Spec.MIN_STACK_INDEX # 17
240+
241+
code = Bytecode()
242+
for i in range(stack_index + 1):
243+
code += Op.PUSH1(i)
244+
code += Op.SWAPN[stack_index]
245+
246+
contract_address = pre.deploy_contract(code=code)
247+
248+
call_code = Op.SSTORE(
249+
0,
250+
Op.CALL(
251+
gas=code.gas_cost(fork) + gas_cost_delta,
252+
address=contract_address,
253+
),
254+
)
255+
call_address = pre.deploy_contract(
256+
code=call_code,
257+
storage={0: 0xDEADBEEF},
258+
)
259+
260+
tx = Transaction(to=call_address, sender=pre.fund_eoa(), gas_limit=200_000)
261+
262+
post = {call_address: Account(storage={0: 0 if gas_cost_delta < 0 else 1})}
263+
264+
state_test(pre=pre, post=post, tx=tx)
265+
266+
267+
@pytest.mark.parametrize(
268+
"invalid_immediate",
269+
list(range(91, 128)), # 0x5b to 0x7f (JUMPDEST and PUSH opcodes)
270+
ids=lambda x: f"swapn_invalid_imm_0x{x:02x}",
271+
)
272+
def test_swapn_invalid_immediate_aborts(
273+
invalid_immediate: int,
274+
pre: Alloc,
275+
state_test: StateTestFiller,
276+
) -> None:
277+
"""
278+
Test SWAPN with invalid immediate values (90 < x < 128) aborts.
279+
280+
Per EIP-8024, immediate values in range [91, 127] (0x5b-0x7f) are
281+
invalid because they correspond to JUMPDEST (0x5b) and PUSH opcodes
282+
(0x60-0x7f). Attempting to execute SWAPN with these immediates should
283+
abort.
284+
"""
285+
sender = pre.fund_eoa()
286+
287+
# Build a stack tall enough for any valid immediate (max decoded index
288+
# is 235, SWAPN needs index + 1 items) so the abort is caused by the
289+
# invalid immediate, never an underflow.
290+
code = Bytecode()
291+
for i in range(Spec.MAX_STACK_INDEX + 1):
292+
code += Op.PUSH1(i % 256)
293+
294+
# Attempt SWAPN with invalid immediate - should abort.
295+
# Pass as bytes (raw immediate byte for testing invalid ranges).
296+
code += Op.SWAPN[invalid_immediate.to_bytes(1, "big")]
297+
298+
# This should never execute.
299+
code += Op.PUSH1(0x42) + Op.PUSH1(0) + Op.SSTORE
300+
code += Op.STOP
301+
302+
contract_address = pre.deploy_contract(code=code)
303+
304+
tx = Transaction(to=contract_address, sender=sender, gas_limit=10_000_000)
305+
306+
# Transaction should fail - invalid immediate causes abort.
307+
post = {contract_address: Account(storage={})}
308+
309+
state_test(pre=pre, post=post, tx=tx)
310+
311+
217312
def test_endofcode_behavior(
218313
pre: Alloc,
219314
state_test: StateTestFiller,

0 commit comments

Comments
 (0)