Skip to content

Commit 56ea9f2

Browse files
bomanapskclowes
andauthored
feat(tests): port ethereum/tests swap opcode tests (#1163)
* Port test for swap opcode * Swap-test-update * store_next_update * Storage update * Fix underflow test by reducing unnecessary stack pushes * Update uv.lock * Lint, add ported_from marker * Add more storage slots to improve coverage * Add coverage_missed_reason * Remove static test swapFiller.yml --------- Co-authored-by: kclowes <[email protected]>
1 parent 50d6bc2 commit 56ea9f2

File tree

2 files changed

+147
-713
lines changed

2 files changed

+147
-713
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""
2+
A State test for the set of `SWAP*` opcodes.
3+
Ported from: https://github.com/ethereum/tests/
4+
blob/develop/src/GeneralStateTestsFiller/VMTests/vmTests/swapFiller.yml.
5+
"""
6+
7+
import pytest # noqa: I001
8+
9+
from ethereum_test_forks import Fork, Frontier, Homestead
10+
from ethereum_test_tools import Account, Alloc, Bytecode, Environment
11+
from ethereum_test_tools import Opcodes as Op
12+
from ethereum_test_tools import StateTestFiller, Storage, Transaction
13+
14+
15+
@pytest.mark.ported_from(
16+
[
17+
"https://github.com/ethereum/tests/blob/v13.3/src/GeneralStateTestsFiller/VMTests/vmTests/swapFiller.yml"
18+
],
19+
pr=["https://github.com/ethereum/execution-spec-tests/pull/1163"],
20+
coverage_missed_reason=(
21+
"Test isolation (1 contract per execution) reduces evmone state "
22+
"comparisons vs old dispatcher pattern (16 contracts per execution)"
23+
),
24+
)
25+
@pytest.mark.parametrize(
26+
"swap_opcode",
27+
[getattr(Op, f"SWAP{i}") for i in range(1, 17)],
28+
ids=lambda op: str(op),
29+
)
30+
@pytest.mark.valid_from("Frontier")
31+
def test_swap(state_test: StateTestFiller, fork: Fork, pre: Alloc, swap_opcode: Op):
32+
"""
33+
The set of `SWAP*` opcodes swaps the top of the stack with a specific
34+
element.
35+
36+
In this test, we ensure that the set of `SWAP*` opcodes correctly swaps
37+
the top element with the nth element and stores the result in storage.
38+
"""
39+
env = Environment()
40+
41+
# Calculate which position we're swapping with (1-based index)
42+
swap_pos = swap_opcode.int() - 0x90 + 1
43+
44+
# Generate stack values
45+
stack_values = list(range(swap_pos + 16))
46+
47+
# Push the stack values onto the stack (in reverse order).
48+
contract_code = Bytecode()
49+
for value in reversed(stack_values):
50+
contract_code += Op.PUSH1(value)
51+
52+
# Perform the SWAP operation.
53+
contract_code += swap_opcode
54+
55+
# Store multiple values to storage.
56+
for slot in range(16):
57+
contract_code += Op.PUSH1(slot) + Op.SSTORE
58+
59+
# Deploy the contract with the generated bytecode.
60+
contract_address = pre.deploy_contract(contract_code)
61+
62+
# Create a transaction to execute the contract.
63+
tx = Transaction(
64+
sender=pre.fund_eoa(),
65+
to=contract_address,
66+
gas_limit=500_000,
67+
protected=False if fork in [Frontier, Homestead] else True,
68+
)
69+
70+
# Calculate expected storage values after SWAP and storage operations
71+
# Initial stack (after pushes, before swap): [0, 1, 2, ..., swap_pos+15]
72+
# (top is index 0)
73+
# After SWAP at position swap_pos: top and position swap_pos are swapped
74+
# Then we do: PUSH1(slot) SSTORE 16 times, which pops values from stack
75+
76+
# Build the stack state after SWAP
77+
stack_after_swap = stack_values.copy()
78+
stack_after_swap[0], stack_after_swap[swap_pos] = (
79+
stack_after_swap[swap_pos],
80+
stack_after_swap[0],
81+
)
82+
83+
# Store the first 16 values from the post-swap stack
84+
storage = Storage()
85+
for value in stack_after_swap[:16]:
86+
storage.store_next(value)
87+
88+
post = {contract_address: Account(storage=storage)}
89+
90+
# Run the state test.
91+
state_test(env=env, pre=pre, post=post, tx=tx)
92+
93+
94+
@pytest.mark.parametrize(
95+
"swap_opcode",
96+
[getattr(Op, f"SWAP{i}") for i in range(1, 17)],
97+
ids=lambda op: str(op),
98+
)
99+
@pytest.mark.valid_from("Frontier")
100+
def test_stack_underflow(
101+
state_test: StateTestFiller,
102+
fork: Fork,
103+
pre: Alloc,
104+
swap_opcode: Op,
105+
):
106+
"""
107+
A test to ensure that the stack underflow when there are not enough
108+
elements for the `SWAP*` opcode to operate.
109+
110+
For each SWAPn operation, we push exactly (n-1) elements to cause an
111+
underflow when trying to swap with the nth element.
112+
"""
113+
env = Environment()
114+
115+
# Calculate which position we're swapping with (1-based index)
116+
swap_pos = swap_opcode.int() - 0x90 + 1
117+
118+
# Push exactly (n-1) elements for SWAPn to cause underflow
119+
contract_code = Bytecode()
120+
for i in range(swap_pos - 1):
121+
contract_code += Op.PUSH1(i % 256)
122+
123+
# Attempt to perform the SWAP operation
124+
contract_code += swap_opcode
125+
126+
# Store the top of the stack in storage slot 0
127+
contract_code += Op.PUSH1(0) + Op.SSTORE
128+
129+
# Deploy the contract with the generated bytecode.
130+
contract = pre.deploy_contract(contract_code)
131+
132+
# Create a transaction to execute the contract.
133+
tx = Transaction(
134+
sender=pre.fund_eoa(),
135+
to=contract,
136+
gas_limit=500_000,
137+
protected=False if fork in [Frontier, Homestead] else True,
138+
)
139+
140+
# Define the expected post-state.
141+
post = {}
142+
storage = Storage()
143+
storage.store_next(0, f"SWAP{swap_pos} failed due to stack underflow")
144+
post[contract] = Account(storage=storage)
145+
146+
# Run the state test.
147+
state_test(env=env, pre=pre, post=post, tx=tx)

0 commit comments

Comments
 (0)