From fe37ca2f0dc57b085274360a16a6ef86fb1b0eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Thu, 7 Nov 2024 17:30:59 +0100 Subject: [PATCH] new(tests): EOF - EIP-3540: validation of opcodes (#932) Add missing opcode tests: - invalid opcode placed after a terminating instruction, - opcodes with truncated immediate bytes. Mark rest of the "opcode tests" as done by providing links to tests. Co-authored-by: Mario Vega Co-authored-by: danceratopz --- converted-ethereum-tests.txt | 4 + .../test_all_opcodes_in_container.py | 116 +++++++++++++++++- tests/osaka/eip7692_eof_v1/eof_tracker.md | 12 +- 3 files changed, 125 insertions(+), 7 deletions(-) diff --git a/converted-ethereum-tests.txt b/converted-ethereum-tests.txt index 195b3f9219..fa95fc194b 100644 --- a/converted-ethereum-tests.txt +++ b/converted-ethereum-tests.txt @@ -9,10 +9,14 @@ GeneralStateTests/stCreate2/call_then_create2_successful_then_returndatasize.jso ([#598](https://github.com/ethereum/execution-spec-tests/pull/598)) EOFTests/EIP3540/validInvalid.json +EOFTests/EIP3670/validInvalid.json EOFTests/efValidation/EOF1_embedded_container_.json EOFTests/efValidation/EOF1_eofcreate_valid_.json EOFTests/efValidation/EOF1_section_order_.json EOFTests/efValidation/EOF1_truncated_section_.json +EOFTests/efValidation/EOF1_undefined_opcodes_.json +EOFTests/efValidation/EOF1_truncated_push_.json +EOFTests/efValidation/deprecated_instructions_.json EOFTests/efValidation/unreachable_code_sections_.json ([#647](https://github.com/ethereum/execution-spec-tests/pull/647)) diff --git a/tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py b/tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py index 14149e069b..b7883cd693 100644 --- a/tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py +++ b/tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py @@ -1,7 +1,8 @@ """ EOF Container: check how every opcode behaves in the middle of the valid eof container code """ -from typing import Any, Dict, List +import itertools +from typing import Any, Dict, Generator, List, Tuple import pytest @@ -10,6 +11,7 @@ from ethereum_test_tools import UndefinedOpcodes from ethereum_test_tools.eof.v1 import Container, ContainerKind, Section from ethereum_test_tools.eof.v1.constants import MAX_OPERAND_STACK_HEIGHT +from ethereum_test_vm import Bytecode from .. import EOF_FORK_NAME @@ -58,6 +60,8 @@ Op.JUMPF, } +data_portion_opcodes = {op for op in all_opcodes if op.has_data_portion()} + # NOTE: `sorted` is used to ensure that the tests are collected in a deterministic order. @@ -118,6 +122,52 @@ def test_all_opcodes_in_container( ) +@pytest.mark.parametrize( + "opcode", + sorted(invalid_eof_opcodes | undefined_opcodes), +) +@pytest.mark.parametrize( + "terminating_opcode", + sorted(halting_opcodes) + [Op.RJUMP], +) +def test_invalid_opcodes_after_stop( + eof_test: EOFTestFiller, + opcode: Opcode, + terminating_opcode: Opcode, +): + """ + Test that an invalid opcode placed after STOP (terminating instruction) invalidates EOF. + """ + terminating_code = Bytecode(terminating_opcode) + match terminating_opcode: # Enhance the code for complex opcodes. + case Op.RETURNCONTRACT: + terminating_code = Op.RETURNCONTRACT[0] + case Op.RETURN | Op.REVERT: + terminating_code = Op.PUSH0 + Op.PUSH0 + terminating_opcode + case Op.RJUMP: + terminating_code = Op.RJUMP[-3] + + eof_code = Container( + kind=ContainerKind.INITCODE + if terminating_opcode == Op.RETURNCONTRACT + else ContainerKind.RUNTIME, + sections=[ + Section.Code(code=terminating_code + opcode), + Section.Data("00" * 32), + ] + + ( + [Section.Container(container=Container.Code(Op.INVALID))] + if terminating_opcode == Op.RETURNCONTRACT + else [] + ), + ) + + eof_test( + data=eof_code, + expect_exception=EOFException.UNDEFINED_INSTRUCTION, + ) + + @pytest.mark.parametrize( "opcode", sorted( @@ -382,3 +432,67 @@ def test_all_opcodes_stack_overflow( data=eof_code, expect_exception=exception, ) + + +def valid_opcode_combinations( + compute_max_stack_height_options: List[bool], + truncate_all_options: List[bool], + opcodes: List[Opcode], +) -> Generator[Tuple[bool, bool, Opcode], None, None]: + """ + Create valid parameter combinations for test_truncated_data_portion_opcodes(). + """ + for opcode, truncate_all, compute_max_stack_height in itertools.product( + opcodes, truncate_all_options, compute_max_stack_height_options + ): + opcode_with_data_portion: bytes = bytes(opcode[1]) + + # Skip invalid or redundant combinations to avoid using pytest.skip in the test + if len(opcode_with_data_portion) == 2 and truncate_all: + continue + if ( + compute_max_stack_height + and max(opcode.min_stack_height, opcode.pushed_stack_items) == 0 + ): + continue + + yield compute_max_stack_height, truncate_all, opcode + + +@pytest.mark.parametrize( + "compute_max_stack_height, truncate_all, opcode", + valid_opcode_combinations([False, True], [False, True], sorted(data_portion_opcodes)), +) +def test_truncated_data_portion_opcodes( + eof_test: EOFTestFiller, + opcode: Opcode, + truncate_all: bool, + compute_max_stack_height: bool, +): + """ + Test that an instruction with data portion and truncated immediate bytes + (therefore a terminating instruction is also missing) invalidates EOF. + """ + opcode_with_data_portion: bytes = bytes(opcode[1]) + + # Compose instruction bytes with empty imm bytes (truncate_all) or 1 byte shorter imm bytes. + opcode_bytes = opcode_with_data_portion[0:1] if truncate_all else opcode_with_data_portion[:-1] + + if opcode.min_stack_height > 0: + opcode_bytes = bytes(Op.PUSH0 * opcode.min_stack_height) + opcode_bytes + + max_stack_height = ( + max(opcode.min_stack_height, opcode.pushed_stack_items) if compute_max_stack_height else 0 + ) + + eof_code = Container( + sections=[ + Section.Code(opcode_bytes, max_stack_height=max_stack_height), + # Provide data section potentially confused with missing imm bytes. + Section.Data(b"\0" * 64), + ] + ) + eof_test( + data=eof_code, + expect_exception=EOFException.TRUNCATED_INSTRUCTION, + ) diff --git a/tests/osaka/eip7692_eof_v1/eof_tracker.md b/tests/osaka/eip7692_eof_v1/eof_tracker.md index a8593dfa07..efacca1499 100644 --- a/tests/osaka/eip7692_eof_v1/eof_tracker.md +++ b/tests/osaka/eip7692_eof_v1/eof_tracker.md @@ -96,12 +96,12 @@ ### Validation -- [ ] Code section with invalid opcodes is rejected (ethereum/tests: ./src/EOFTestsFiller/efExample/validInvalidFiller.yml src/EOFTestsFiller/efValidation/EOF1_undefined_opcodes_Copier.json src/EOFTestsFiller/EIP3670/validInvalidFiller.yml) -- [ ] INVALID opcode is valid (ethereum/tests: ./src/EOFTestsFiller/efExample/validInvalidFiller.yml) -- [ ] Truncated PUSH data (ethereum/tests: ./src/EOFTestsFiller/efExample/validInvalidFiller.yml src/EOFTestsFiller/efValidation/EOF1_truncated_push_Copier.json src/EOFTestsFiller/EIP3670/validInvalidFiller.yml) -- [ ] Opcodes deprecated in EOF are rejected (ethereum/tests: src/EOFTestsFiller/efValidation/deprecated_instructions_Copier.json ethereum/tests: src/EOFTestsFiller/EIP3670/validInvalidFiller.yml) -- [ ] Codes with each valid opcodes (ethereum/tests: src/EOFTestsFiller/EIP3670/validInvalidFiller.yml) -- [ ] Undefined instruction after terminating instruction (ethereum/tests: src/EOFTestsFiller/EIP3670/validInvalidFiller.yml) +- [x] Code section with invalid opcodes is rejected ([`tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py::test_all_opcodes_in_container`](./eip3540_eof_v1/test_all_opcodes_in_container/test_all_opcodes_in_container.md)) +- [x] INVALID opcode is valid ([`tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py::test_all_opcodes_in_container`](./eip3540_eof_v1/test_all_opcodes_in_container/test_all_opcodes_in_container.md)) +- [x] Truncated PUSH data ([`tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py::test_truncated_data_portion_opcodes`](./eip3540_eof_v1/test_all_opcodes_in_container/test_truncated_data_portion_opcodes.md)) +- [x] Opcodes deprecated in EOF are rejected ([`tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py::test_all_opcodes_in_container`](./eip3540_eof_v1/test_all_opcodes_in_container/test_all_opcodes_in_container.md)) +- [x] Codes with each valid opcodes ([`tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py::test_all_opcodes_in_container`](./eip3540_eof_v1/test_all_opcodes_in_container/test_all_opcodes_in_container.md)) +- [x] Undefined instruction after terminating instruction ([`tests/osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py::test_invalid_opcodes_after_stop`](./eip3540_eof_v1/test_all_opcodes_in_container/test_invalid_opcodes_after_stop.md)) ## EIP-4200: EOF - Static relative jumps