From fd7361ac21478b140c581a56381bb32f501ad392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Thu, 23 May 2024 15:57:03 +0200 Subject: [PATCH] new(test): add tests for EOF/EIP-663 DUPN SWAPN (#502) * new(test): add tests for EOF/EIP-663 DUPN SWAPN * new(fw): EOF stack exceptions * fix(tests): eip-7692: spec sha * new(tests): eip-7692: dupn negative tests * new(tests): eip-7692: parametrize * new(tests): eip-7692: more swapn tests * docs: changelog --------- Co-authored-by: Mario Vega --- docs/CHANGELOG.md | 1 + .../exceptions/evmone_exceptions.py | 5 + .../exceptions/exceptions.py | 12 ++ .../eip663_dupn_swapn_exchange/__init__.py | 7 + .../eip663_dupn_swapn_exchange/conftest.py | 14 ++ .../eip663_dupn_swapn_exchange/test_dupn.py | 139 ++++++++++++++++++ .../eip663_dupn_swapn_exchange/test_swapn.py | 131 +++++++++++++++++ tests/prague/eip7692_eof_v1/tracker.md | 12 ++ whitelist.txt | 2 + 9 files changed, 323 insertions(+) create mode 100644 tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/__init__.py create mode 100644 tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/conftest.py create mode 100644 tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_dupn.py create mode 100644 tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_swapn.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d1d1b3876c..d250709666 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add `test_double_kill` and `test_recreate` which test resurrection of accounts killed with `SELFDESTRUCT` ([#488](https://github.com/ethereum/execution-spec-tests/pull/488)). - ✨ Add eof example valid invalid tests from ori, fetch EOF Container implementation ([#535](https://github.com/ethereum/execution-spec-tests/pull/535)). +- ✨ [EIP-663](https://eips.ethereum.org/EIPS/eip-663): Add `test_dupn.py` and `test_swapn.py` ([#502](https://github.com/ethereum/execution-spec-tests/pull/502)). ### 🛠️ Framework diff --git a/src/ethereum_test_tools/exceptions/evmone_exceptions.py b/src/ethereum_test_tools/exceptions/evmone_exceptions.py index 2b1b8f8f47..6f23356fbd 100644 --- a/src/ethereum_test_tools/exceptions/evmone_exceptions.py +++ b/src/ethereum_test_tools/exceptions/evmone_exceptions.py @@ -49,6 +49,11 @@ class EvmoneExceptionMapper: ExceptionMessage(EOFException.UNREACHABLE_INSTRUCTIONS, "err: unreachable_instructions"), ExceptionMessage(EOFException.INVALID_RJUMP_DESTINATION, "err: invalid_rjump_destination"), ExceptionMessage(EOFException.UNREACHABLE_CODE_SECTIONS, "err: unreachable_code_sections"), + ExceptionMessage(EOFException.STACK_UNDERFLOW, "err: stack_underflow"), + ExceptionMessage( + EOFException.MAX_STACK_HEIGHT_ABOVE_LIMIT, "err: max_stack_height_above_limit" + ), + ExceptionMessage(EOFException.INVALID_MAX_STACK_HEIGHT, "err: invalid_max_stack_height"), ExceptionMessage(EOFException.INVALID_DATALOADN_INDEX, "err: invalid_dataloadn_index"), ) diff --git a/src/ethereum_test_tools/exceptions/exceptions.py b/src/ethereum_test_tools/exceptions/exceptions.py index e32cbed23e..8defeaf21e 100644 --- a/src/ethereum_test_tools/exceptions/exceptions.py +++ b/src/ethereum_test_tools/exceptions/exceptions.py @@ -303,6 +303,18 @@ class EOFException(ExceptionBase): """ EOF container's body have code sections that are unreachable """ + STACK_UNDERFLOW = auto() + """ + EOF container's code produces an stack underflow + """ + MAX_STACK_HEIGHT_ABOVE_LIMIT = auto() + """ + EOF container's specified max stack height is above the limit + """ + INVALID_MAX_STACK_HEIGHT = auto() + """ + EOF container section's specified max stack height does not match the actual stack height + """ INVALID_DATALOADN_INDEX = auto() """ A DATALOADN instruction has out-of-bounds index for the data section diff --git a/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/__init__.py b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/__init__.py new file mode 100644 index 0000000000..a25957ce27 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/__init__.py @@ -0,0 +1,7 @@ +""" +abstract: Tests [EIP-663: SWAPN, DUPN and EXCHANGE instructions](https://eips.ethereum.org/EIPS/eip-663) + Tests for [EIP-663: SWAPN, DUPN and EXCHANGE instructions](https://eips.ethereum.org/EIPS/eip-663). +""" # noqa: E501 + +REFERENCE_SPEC_GIT_PATH = "EIPS/eip-663.md" +REFERENCE_SPEC_VERSION = "b658bb87fe039d29e9475d5cfaebca9b92e0fca2" diff --git a/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/conftest.py b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/conftest.py new file mode 100644 index 0000000000..9c7b2de037 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/conftest.py @@ -0,0 +1,14 @@ +""" +Pytest fixtures for EIP-663 tests +""" +import pytest + +from ethereum_test_tools import Transaction + + +@pytest.fixture +def tx() -> Transaction: + """ + Produces the default Transaction. + """ + return Transaction(to=0xC0DE, gas_limit=10_000_000) diff --git a/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_dupn.py b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_dupn.py new file mode 100644 index 0000000000..0a254e44a7 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_dupn.py @@ -0,0 +1,139 @@ +""" +abstract: Tests [EIP-663: SWAPN, DUPN and EXCHANGE instructions](https://eips.ethereum.org/EIPS/eip-663) + Tests for the DUPN instruction. +""" # noqa: E501 + +import pytest + +from ethereum_test_tools import ( + Account, + Environment, + EOFException, + EOFTestFiller, + StateTestFiller, + TestAddress, + Transaction, +) +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import MAX_OPERAND_STACK_HEIGHT, NON_RETURNING_SECTION +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from ..eip3540_eof_v1.spec import EOF_FORK_NAME +from . import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION + +REFERENCE_SPEC_GIT_PATH = REFERENCE_SPEC_GIT_PATH +REFERENCE_SPEC_VERSION = REFERENCE_SPEC_VERSION + + +@pytest.mark.valid_from(EOF_FORK_NAME) +def test_dupn_all_valid_immediates( + tx: Transaction, + state_test: StateTestFiller, +): + """ + Test case for all valid DUPN immediates. + """ + n = 2**8 + values = range(0xD00, 0xD00 + n) + + eof_code = Container( + sections=[ + Section.Code( + code=b"".join(Op.PUSH2(v) for v in values) + + b"".join(Op.SSTORE(x, Op.DUPN[x]) for x in range(0, n)) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=n + 2, + ) + ], + ) + + pre = { + TestAddress: Account(balance=1_000_000_000), + tx.to: Account(code=eof_code), + } + + post = {tx.to: Account(storage=dict(zip(range(0, n), reversed(values))))} + + state_test( + env=Environment(), + pre=pre, + post=post, + tx=tx, + ) + + +@pytest.mark.parametrize( + "stack_height,max_stack_height", + [ + [0, 0], + [0, 1], + [1, 1], + [1, 2], + [2**8 - 1, 2**8 - 1], + [2**8 - 1, 2**8], + ], +) +@pytest.mark.valid_from(EOF_FORK_NAME) +def test_dupn_stack_underflow( + stack_height: int, + max_stack_height: int, + eof_test: EOFTestFiller, +): + """ + Test case out of bounds DUPN immediate. + """ + eof_code = Container( + sections=[ + Section.Code( + code=b"".join(Op.PUSH2(v) for v in range(0, stack_height)) + + Op.DUPN[stack_height] + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=max_stack_height, + ) + ], + ) + eof_test( + data=eof_code, + expect_exception=EOFException.STACK_UNDERFLOW, + ) + + +@pytest.mark.parametrize( + "dupn_operand,max_stack_height,expect_exception", + [ + [0, MAX_OPERAND_STACK_HEIGHT, EOFException.INVALID_MAX_STACK_HEIGHT], + [0, MAX_OPERAND_STACK_HEIGHT + 1, EOFException.MAX_STACK_HEIGHT_ABOVE_LIMIT], + [2**8 - 1, MAX_OPERAND_STACK_HEIGHT, EOFException.INVALID_MAX_STACK_HEIGHT], + [2**8 - 1, MAX_OPERAND_STACK_HEIGHT + 1, EOFException.MAX_STACK_HEIGHT_ABOVE_LIMIT], + ], +) +@pytest.mark.valid_from(EOF_FORK_NAME) +def test_dupn_stack_overflow( + dupn_operand: int, + max_stack_height: int, + expect_exception: EOFException, + eof_test: EOFTestFiller, +): + """ + Test case where DUPN produces an stack overflow. + """ + eof_code = Container( + sections=[ + Section.Code( + code=b"".join(Op.PUSH2(v) for v in range(0, MAX_OPERAND_STACK_HEIGHT)) + + Op.DUPN[dupn_operand] + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=max_stack_height, + ) + ], + ) + eof_test( + data=eof_code, + expect_exception=expect_exception, + ) diff --git a/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_swapn.py b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_swapn.py new file mode 100644 index 0000000000..97b7e05c31 --- /dev/null +++ b/tests/prague/eip7692_eof_v1/eip663_dupn_swapn_exchange/test_swapn.py @@ -0,0 +1,131 @@ +""" +abstract: Tests [EIP-663: SWAPN, DUPN and EXCHANGE instructions](https://eips.ethereum.org/EIPS/eip-663) + Tests for the SWAPN instruction. +""" # noqa: E501 + +import pytest + +from ethereum_test_tools import ( + Account, + Environment, + EOFException, + EOFTestFiller, + StateTestFiller, + TestAddress, + Transaction, +) +from ethereum_test_tools.eof.v1 import Container, Section +from ethereum_test_tools.eof.v1.constants import MAX_OPERAND_STACK_HEIGHT, NON_RETURNING_SECTION +from ethereum_test_tools.vm.opcode import Opcodes as Op + +from ..eip3540_eof_v1.spec import EOF_FORK_NAME +from . import REFERENCE_SPEC_GIT_PATH, REFERENCE_SPEC_VERSION + +REFERENCE_SPEC_GIT_PATH = REFERENCE_SPEC_GIT_PATH +REFERENCE_SPEC_VERSION = REFERENCE_SPEC_VERSION + + +@pytest.mark.valid_from(EOF_FORK_NAME) +def test_swapn_all_valid_immediates( + tx: Transaction, + state_test: StateTestFiller, +): + """ + Test case for all valid SWAPN immediates. + """ + n = 256 + values = range(0x500, 0x500 + 257) + + eof_code = Container( + sections=[ + Section.Code( + code=b"".join(Op.PUSH2(v) for v in values) + + b"".join(Op.SSTORE(x, Op.SWAPN[0xFF - x]) for x in range(0, n)) + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=n + 2, + ) + ], + ) + + pre = { + TestAddress: Account(balance=1_000_000_000), + tx.to: Account(code=eof_code), + } + + values_rotated = list(values[1:]) + [values[0]] + post = {tx.to: Account(storage=dict(zip(range(0, n), reversed(values_rotated))))} + + state_test( + env=Environment(), + pre=pre, + post=post, + tx=tx, + ) + + +@pytest.mark.parametrize( + "swapn_operand", + [ + 0, + 2**8 - 1, + ], +) +@pytest.mark.valid_from(EOF_FORK_NAME) +def test_swapn_on_max_stack( + swapn_operand: int, + eof_test: EOFTestFiller, +): + """ + Test case out of bounds DUPN immediate. + """ + eof_code = Container( + sections=[ + Section.Code( + code=b"".join(Op.PUSH2(v) for v in range(0, MAX_OPERAND_STACK_HEIGHT)) + + Op.SWAPN[swapn_operand] + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=MAX_OPERAND_STACK_HEIGHT, + ) + ], + ) + eof_test( + data=eof_code, + ) + + +@pytest.mark.parametrize( + "stack_height", + [ + 0, + 1, + 2**8 - 1, + ], +) +@pytest.mark.valid_from(EOF_FORK_NAME) +def test_swapn_stack_underflow( + stack_height: int, + eof_test: EOFTestFiller, +): + """ + Test case out of bounds DUPN immediate. + """ + eof_code = Container( + sections=[ + Section.Code( + code=b"".join(Op.PUSH2(v) for v in range(0, stack_height)) + + Op.SWAPN[stack_height] + + Op.STOP, + code_inputs=0, + code_outputs=NON_RETURNING_SECTION, + max_stack_height=MAX_OPERAND_STACK_HEIGHT, + ) + ], + ) + eof_test( + data=eof_code, + expect_exception=EOFException.STACK_UNDERFLOW, + ) diff --git a/tests/prague/eip7692_eof_v1/tracker.md b/tests/prague/eip7692_eof_v1/tracker.md index 8ea5b7ccd4..274134d37e 100644 --- a/tests/prague/eip7692_eof_v1/tracker.md +++ b/tests/prague/eip7692_eof_v1/tracker.md @@ -117,6 +117,18 @@ ## EIP-663: SWAPN, DUPN and EXCHANGE instructions +### Validation + +- [ ] A DUPN instruction causes stack overflow +- [ ] A DUPN instruction causes stack underflow +- [ ] A DUPN instruction causes max stack height mismatch +- [ ] A SWAPN instruction causes stack underflow + +### Execution + +- [x] Positive tests for DUPN instructions (./eip663_dupn_swapn_exchange/test_dupn.py::test_dupn_all_valid_immediates) +- [x] Positive tests for SWAPN instructions (./eip663_dupn_swapn_exchange/test_swapn.py::test_swapn_all_valid_immediates) + ## EIP-7069: Revamped CALL instructions ## EIP-7620: EOF Contract Creation diff --git a/whitelist.txt b/whitelist.txt index c3be30aaf8..d2b1ada8eb 100644 --- a/whitelist.txt +++ b/whitelist.txt @@ -91,6 +91,7 @@ dup dunder EEST eip +eip3540 eips EIPs endianness @@ -163,6 +164,7 @@ hyperledger iat ignoreRevsFile img +immediates incrementing init initcode