diff --git a/tests/frontier/opcodes/test_swap.py b/tests/frontier/opcodes/test_swap.py new file mode 100644 index 00000000000..d527b41b6fd --- /dev/null +++ b/tests/frontier/opcodes/test_swap.py @@ -0,0 +1,129 @@ +""" +A State test for the set of `SWAP*` opcodes. +Ported from: https://github.com/ethereum/tests/ +blob/develop/src/GeneralStateTestsFiller/VMTests/vmTests/swapFiller.yml. +""" + +import pytest # noqa: I001 + +from ethereum_test_forks import Fork, Frontier, Homestead +from ethereum_test_tools import Account, Alloc, Bytecode, Environment +from ethereum_test_tools import Opcodes as Op +from ethereum_test_tools import StateTestFiller, Storage, Transaction + + +@pytest.mark.parametrize( + "swap_opcode", + [getattr(Op, f"SWAP{i}") for i in range(1, 17)], + ids=lambda op: str(op), +) +@pytest.mark.valid_from("Frontier") +def test_swap( + state_test: StateTestFiller, + fork: Fork, + pre: Alloc, + swap_opcode: Op +): + """ + The set of `SWAP*` opcodes swaps the top of the stack with a specific + element. + + In this test, we ensure that the set of `SWAP*` opcodes correctly swaps + the top element with the nth element and stores the result in storage. + """ + env = Environment() + + # Calculate which position we're swapping with (1-based index) + swap_pos = swap_opcode.int() - 0x90 + 1 + + # Generate stack values + stack_values = list(range(swap_pos + 1)) + + # Push the stack values onto the stack (in reverse order). + contract_code = Bytecode() + for value in reversed(stack_values): + contract_code += Op.PUSH1(value) + + # Perform the SWAP operation. + contract_code += swap_opcode + + # Store the top of the stack in storage slot 0. + contract_code += Op.PUSH1(0) + Op.SSTORE + + # Deploy the contract with the generated bytecode. + contract = pre.deploy_contract(contract_code) + + # Create a transaction to execute the contract. + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + gas_limit=500_000, + protected=False if fork in [Frontier, Homestead] else True, + ) + + # After SWAP, the top value will be the one at swap_pos + expected_value = swap_pos + + # Define the expected post-state. + post = {} + storage = Storage() + storage.store_next(expected_value, f"SWAP{swap_pos} result") + post[contract] = Account(storage=storage) + # Run the state test. + state_test(env=env, pre=pre, post=post, tx=tx) + + +@pytest.mark.parametrize( + "swap_opcode", + [getattr(Op, f"SWAP{i}") for i in range(1, 17)], + ids=lambda op: str(op), +) +@pytest.mark.valid_from("Frontier") +def test_stack_underflow( + state_test: StateTestFiller, + fork: Fork, + pre: Alloc, + swap_opcode: Op, +): + """ + A test to ensure that the stack underflow when there are not enough + elements for the `SWAP*` opcode to operate. + + For each SWAPn operation, we push exactly (n-1) elements to cause an + underflow when trying to swap with the nth element. + """ + env = Environment() + + # Calculate which position we're swapping with (1-based index) + swap_pos = swap_opcode.int() - 0x90 + 1 + + # Push exactly (n-1) elements for SWAPn to cause underflow + contract_code = Bytecode() + for i in range(swap_pos - 1): + contract_code += Op.PUSH1(i % 256) + + # Attempt to perform the SWAP operation + contract_code += swap_opcode + + # Store the top of the stack in storage slot 0 + contract_code += Op.PUSH1(0) + Op.SSTORE + + # Deploy the contract with the generated bytecode. + contract = pre.deploy_contract(contract_code) + + # Create a transaction to execute the contract. + tx = Transaction( + sender=pre.fund_eoa(), + to=contract, + gas_limit=500_000, + protected=False if fork in [Frontier, Homestead] else True, + ) + + # Define the expected post-state. + post = {} + storage = Storage() + storage.store_next(0, f"SWAP{swap_pos} failed due to stack underflow") + post[contract] = Account(storage=storage) + + # Run the state test. + state_test(env=env, pre=pre, post=post, tx=tx) diff --git a/uv.lock b/uv.lock index b0c8125b74b..baf82e056e4 100644 --- a/uv.lock +++ b/uv.lock @@ -594,7 +594,7 @@ requires-dist = [ { name = "pytest-regex", specifier = ">=0.2.0,<0.3" }, { name = "pytest-xdist", specifier = ">=3.3.1,<4" }, { name = "pyyaml", specifier = ">=6.0.2,<7" }, - { name = "questionary", specifier = ">=2.1.0,<3" }, + { name = "questionary", git = "https://github.com/tmbo/questionary?rev=ff22aeae1cd9c1c734f14329934e349bec7873bc" }, { name = "requests", specifier = ">=2.31.0,<3" }, { name = "requests-unixsocket2", specifier = ">=0.4.0" }, { name = "rich", specifier = ">=13.7.0,<14" },