|
1 | 1 | """ |
2 | 2 | EOF Container: check how every opcode behaves in the middle of the valid eof container code |
3 | 3 | """ |
4 | | -from typing import Any, Dict, List |
| 4 | +import itertools |
| 5 | +from typing import Any, Dict, Generator, List, Tuple |
5 | 6 |
|
6 | 7 | import pytest |
7 | 8 |
|
|
10 | 11 | from ethereum_test_tools import UndefinedOpcodes |
11 | 12 | from ethereum_test_tools.eof.v1 import Container, ContainerKind, Section |
12 | 13 | from ethereum_test_tools.eof.v1.constants import MAX_OPERAND_STACK_HEIGHT |
| 14 | +from ethereum_test_vm import Bytecode |
13 | 15 |
|
14 | 16 | from .. import EOF_FORK_NAME |
15 | 17 |
|
|
58 | 60 | Op.JUMPF, |
59 | 61 | } |
60 | 62 |
|
| 63 | +data_portion_opcodes = {op for op in all_opcodes if op.has_data_portion()} |
| 64 | + |
61 | 65 |
|
62 | 66 | # NOTE: `sorted` is used to ensure that the tests are collected in a deterministic order. |
63 | 67 |
|
@@ -118,6 +122,52 @@ def test_all_opcodes_in_container( |
118 | 122 | ) |
119 | 123 |
|
120 | 124 |
|
| 125 | +@pytest.mark.parametrize( |
| 126 | + "opcode", |
| 127 | + sorted(invalid_eof_opcodes | undefined_opcodes), |
| 128 | +) |
| 129 | +@pytest.mark.parametrize( |
| 130 | + "terminating_opcode", |
| 131 | + sorted(halting_opcodes) + [Op.RJUMP], |
| 132 | +) |
| 133 | +def test_invalid_opcodes_after_stop( |
| 134 | + eof_test: EOFTestFiller, |
| 135 | + opcode: Opcode, |
| 136 | + terminating_opcode: Opcode, |
| 137 | +): |
| 138 | + """ |
| 139 | + Test that an invalid opcode placed after STOP (terminating instruction) invalidates EOF. |
| 140 | + """ |
| 141 | + terminating_code = Bytecode(terminating_opcode) |
| 142 | + match terminating_opcode: # Enhance the code for complex opcodes. |
| 143 | + case Op.RETURNCONTRACT: |
| 144 | + terminating_code = Op.RETURNCONTRACT[0] |
| 145 | + case Op.RETURN | Op.REVERT: |
| 146 | + terminating_code = Op.PUSH0 + Op.PUSH0 + terminating_opcode |
| 147 | + case Op.RJUMP: |
| 148 | + terminating_code = Op.RJUMP[-3] |
| 149 | + |
| 150 | + eof_code = Container( |
| 151 | + kind=ContainerKind.INITCODE |
| 152 | + if terminating_opcode == Op.RETURNCONTRACT |
| 153 | + else ContainerKind.RUNTIME, |
| 154 | + sections=[ |
| 155 | + Section.Code(code=terminating_code + opcode), |
| 156 | + Section.Data("00" * 32), |
| 157 | + ] |
| 158 | + + ( |
| 159 | + [Section.Container(container=Container.Code(Op.INVALID))] |
| 160 | + if terminating_opcode == Op.RETURNCONTRACT |
| 161 | + else [] |
| 162 | + ), |
| 163 | + ) |
| 164 | + |
| 165 | + eof_test( |
| 166 | + data=eof_code, |
| 167 | + expect_exception=EOFException.UNDEFINED_INSTRUCTION, |
| 168 | + ) |
| 169 | + |
| 170 | + |
121 | 171 | @pytest.mark.parametrize( |
122 | 172 | "opcode", |
123 | 173 | sorted( |
@@ -382,3 +432,67 @@ def test_all_opcodes_stack_overflow( |
382 | 432 | data=eof_code, |
383 | 433 | expect_exception=exception, |
384 | 434 | ) |
| 435 | + |
| 436 | + |
| 437 | +def valid_opcode_combinations( |
| 438 | + compute_max_stack_height_options: List[bool], |
| 439 | + truncate_all_options: List[bool], |
| 440 | + opcodes: List[Opcode], |
| 441 | +) -> Generator[Tuple[bool, bool, Opcode], None, None]: |
| 442 | + """ |
| 443 | + Create valid parameter combinations for test_truncated_data_portion_opcodes(). |
| 444 | + """ |
| 445 | + for opcode, truncate_all, compute_max_stack_height in itertools.product( |
| 446 | + opcodes, truncate_all_options, compute_max_stack_height_options |
| 447 | + ): |
| 448 | + opcode_with_data_portion: bytes = bytes(opcode[1]) |
| 449 | + |
| 450 | + # Skip invalid or redundant combinations to avoid using pytest.skip in the test |
| 451 | + if len(opcode_with_data_portion) == 2 and truncate_all: |
| 452 | + continue |
| 453 | + if ( |
| 454 | + compute_max_stack_height |
| 455 | + and max(opcode.min_stack_height, opcode.pushed_stack_items) == 0 |
| 456 | + ): |
| 457 | + continue |
| 458 | + |
| 459 | + yield compute_max_stack_height, truncate_all, opcode |
| 460 | + |
| 461 | + |
| 462 | +@pytest.mark.parametrize( |
| 463 | + "compute_max_stack_height, truncate_all, opcode", |
| 464 | + valid_opcode_combinations([False, True], [False, True], sorted(data_portion_opcodes)), |
| 465 | +) |
| 466 | +def test_truncated_data_portion_opcodes( |
| 467 | + eof_test: EOFTestFiller, |
| 468 | + opcode: Opcode, |
| 469 | + truncate_all: bool, |
| 470 | + compute_max_stack_height: bool, |
| 471 | +): |
| 472 | + """ |
| 473 | + Test that an instruction with data portion and truncated immediate bytes |
| 474 | + (therefore a terminating instruction is also missing) invalidates EOF. |
| 475 | + """ |
| 476 | + opcode_with_data_portion: bytes = bytes(opcode[1]) |
| 477 | + |
| 478 | + # Compose instruction bytes with empty imm bytes (truncate_all) or 1 byte shorter imm bytes. |
| 479 | + opcode_bytes = opcode_with_data_portion[0:1] if truncate_all else opcode_with_data_portion[:-1] |
| 480 | + |
| 481 | + if opcode.min_stack_height > 0: |
| 482 | + opcode_bytes = bytes(Op.PUSH0 * opcode.min_stack_height) + opcode_bytes |
| 483 | + |
| 484 | + max_stack_height = ( |
| 485 | + max(opcode.min_stack_height, opcode.pushed_stack_items) if compute_max_stack_height else 0 |
| 486 | + ) |
| 487 | + |
| 488 | + eof_code = Container( |
| 489 | + sections=[ |
| 490 | + Section.Code(opcode_bytes, max_stack_height=max_stack_height), |
| 491 | + # Provide data section potentially confused with missing imm bytes. |
| 492 | + Section.Data(b"\0" * 64), |
| 493 | + ] |
| 494 | + ) |
| 495 | + eof_test( |
| 496 | + data=eof_code, |
| 497 | + expect_exception=EOFException.TRUNCATED_INSTRUCTION, |
| 498 | + ) |
0 commit comments