Skip to content

Commit a9ecfc8

Browse files
chfastmarioevzdanceratopz
authored
new(tests): EOF - EIP-3540: validation of opcodes (ethereum#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 <[email protected]> Co-authored-by: danceratopz <[email protected]>
1 parent 63a49c8 commit a9ecfc8

File tree

2 files changed

+121
-7
lines changed

2 files changed

+121
-7
lines changed

osaka/eip7692_eof_v1/eip3540_eof_v1/test_all_opcodes_in_container.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
EOF Container: check how every opcode behaves in the middle of the valid eof container code
33
"""
4-
from typing import Any, Dict, List
4+
import itertools
5+
from typing import Any, Dict, Generator, List, Tuple
56

67
import pytest
78

@@ -10,6 +11,7 @@
1011
from ethereum_test_tools import UndefinedOpcodes
1112
from ethereum_test_tools.eof.v1 import Container, ContainerKind, Section
1213
from ethereum_test_tools.eof.v1.constants import MAX_OPERAND_STACK_HEIGHT
14+
from ethereum_test_vm import Bytecode
1315

1416
from .. import EOF_FORK_NAME
1517

@@ -58,6 +60,8 @@
5860
Op.JUMPF,
5961
}
6062

63+
data_portion_opcodes = {op for op in all_opcodes if op.has_data_portion()}
64+
6165

6266
# NOTE: `sorted` is used to ensure that the tests are collected in a deterministic order.
6367

@@ -118,6 +122,52 @@ def test_all_opcodes_in_container(
118122
)
119123

120124

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+
121171
@pytest.mark.parametrize(
122172
"opcode",
123173
sorted(
@@ -382,3 +432,67 @@ def test_all_opcodes_stack_overflow(
382432
data=eof_code,
383433
expect_exception=exception,
384434
)
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+
)

osaka/eip7692_eof_v1/eof_tracker.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,12 @@
9696

9797
### Validation
9898

99-
- [ ] 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)
100-
- [ ] INVALID opcode is valid (ethereum/tests: ./src/EOFTestsFiller/efExample/validInvalidFiller.yml)
101-
- [ ] Truncated PUSH data (ethereum/tests: ./src/EOFTestsFiller/efExample/validInvalidFiller.yml src/EOFTestsFiller/efValidation/EOF1_truncated_push_Copier.json src/EOFTestsFiller/EIP3670/validInvalidFiller.yml)
102-
- [ ] Opcodes deprecated in EOF are rejected (ethereum/tests: src/EOFTestsFiller/efValidation/deprecated_instructions_Copier.json ethereum/tests: src/EOFTestsFiller/EIP3670/validInvalidFiller.yml)
103-
- [ ] Codes with each valid opcodes (ethereum/tests: src/EOFTestsFiller/EIP3670/validInvalidFiller.yml)
104-
- [ ] Undefined instruction after terminating instruction (ethereum/tests: src/EOFTestsFiller/EIP3670/validInvalidFiller.yml)
99+
- [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))
100+
- [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))
101+
- [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))
102+
- [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))
103+
- [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))
104+
- [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))
105105

106106
## EIP-4200: EOF - Static relative jumps
107107

0 commit comments

Comments
 (0)