diff --git a/lists/evm/pending-opcodes.md b/lists/evm/pending-opcodes.md index 300630c0aa9..27224302291 100644 --- a/lists/evm/pending-opcodes.md +++ b/lists/evm/pending-opcodes.md @@ -11,6 +11,7 @@ next or subsequent hard fork. | 0x5C | TLOAD | Transient data load | [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153) | | 0x5D | TSTORE | Transient data store | [EIP-1153](https://eips.ethereum.org/EIPS/eip-1153) | | 0x5E | MCOPY | Memory copy | [EIP-5656](https://eips.ethereum.org/EIPS/eip-5656) | +| 0xAE | EXTENSION | Extension prefix for non-Ethereum-L1 EVM chains | [EIP-8163](https://eips.ethereum.org/EIPS/eip-8163) | | 0xD0 | DATALOAD | Loads data from EOF data section, via stack | [EIP-7480](https://eips.ethereum.org/EIPS/eip-7480) | | 0xD1 | DATALOADN | Loads data from EOF data section, via immediate | [EIP-7480](https://eips.ethereum.org/EIPS/eip-7480) | | 0xD2 | DATASIZE | Size of the EOF data section | [EIP-7480](https://eips.ethereum.org/EIPS/eip-7480) | @@ -222,7 +223,7 @@ scheduled or accepted) are in *italics*. | 0xAB | | | | | | 0xAC | | | | | | 0xAD | | | | | -| 0xAE | | | | | +| *0xAE* | *EXTENSION* | *System* | *Bogota* | *[EIP-8163](https://eips.ethereum.org/EIPS/eip-8163)* | | 0xAF | | | | | | 0xB0 | | | | | | 0xB1 | | | | | diff --git a/lists/evm/proposed-opcodes.md b/lists/evm/proposed-opcodes.md index 5692aedc230..2f1dcd71ba5 100644 --- a/lists/evm/proposed-opcodes.md +++ b/lists/evm/proposed-opcodes.md @@ -79,3 +79,4 @@ unshipped EIPs, even withdrawn and non-viable proposals. | [EIP-7620](https://eips.ethereum.org/EIPS/eip-7620) | 0xEC | EOFCREATE | Create from EOF contained initcode | | [EIP-7620](https://eips.ethereum.org/EIPS/eip-7620) | 0xED | TXCREATE | Create from transaction contained initcode (removed from EIP-7620) | | [EIP-7620](https://eips.ethereum.org/EIPS/eip-7620) | 0xEE | RETURNCONTRACT | Contract to be created, references EOF data | +| [EIP-8163](https://eips.ethereum.org/EIPS/eip-8163) | 0xAE | EXTENSION | Extension prefix for non-Ethereum-L1 EVM chains | diff --git a/packages/testing/src/execution_testing/cli/evm_bytes.py b/packages/testing/src/execution_testing/cli/evm_bytes.py index 3929ad4dce8..5827037bdb4 100644 --- a/packages/testing/src/execution_testing/cli/evm_bytes.py +++ b/packages/testing/src/execution_testing/cli/evm_bytes.py @@ -12,6 +12,7 @@ Op.STOP, Op.REVERT, Op.INVALID, + Op.EXTENSION, Op.JUMP, Op.JUMPI, } diff --git a/packages/testing/src/execution_testing/cli/tests/test_evm_bytes.py b/packages/testing/src/execution_testing/cli/tests/test_evm_bytes.py index d5622cd7d86..6cc5ec78285 100644 --- a/packages/testing/src/execution_testing/cli/tests/test_evm_bytes.py +++ b/packages/testing/src/execution_testing/cli/tests/test_evm_bytes.py @@ -164,7 +164,7 @@ def test_individual_opcodes(opcode: Op) -> None: else: expected_output = f"Op.{opcode._name_}" - bytecode = opcode.int().to_bytes(1, byteorder="big") + data_portion + bytecode = bytes(opcode) + data_portion assert process_evm_bytes_string("0x" + bytecode.hex()) == expected_output diff --git a/packages/testing/src/execution_testing/forks/__init__.py b/packages/testing/src/execution_testing/forks/__init__.py index 88a7fe529fa..2b08ce44d04 100644 --- a/packages/testing/src/execution_testing/forks/__init__.py +++ b/packages/testing/src/execution_testing/forks/__init__.py @@ -126,6 +126,7 @@ "ArrowGlacier", "Berlin", "BerlinToLondonAt5", + "Bogota", "Byzantium", "Constantinople", "ConstantinopleFix", diff --git a/packages/testing/src/execution_testing/forks/forks/eips/bogota/eip_8163.py b/packages/testing/src/execution_testing/forks/forks/eips/bogota/eip_8163.py new file mode 100644 index 00000000000..f89f26c821f --- /dev/null +++ b/packages/testing/src/execution_testing/forks/forks/eips/bogota/eip_8163.py @@ -0,0 +1,29 @@ +""" +EIP-8163: Reserve EXTENSION (0xae) opcode. + +Reserve `EXTENSION` as an extension prefix for EVM chains other than +Ethereum L1, where it behaves exactly like `INVALID`. + +https://eips.ethereum.org/EIPS/eip-8163 +""" + +from typing import Callable, Dict + +from execution_testing.vm import OpcodeBase, Opcodes + +from ....base_fork import BaseFork + + +class EIP8163(BaseFork): + """EIP-8163 class.""" + + @classmethod + def opcode_gas_map( + cls, + ) -> Dict[OpcodeBase, int | Callable[[OpcodeBase], int]]: + """Add EXTENSION, which halts exceptionally like INVALID.""" + base_map = super(EIP8163, cls).opcode_gas_map() + return { + **base_map, + Opcodes.EXTENSION: 0, + } diff --git a/packages/testing/src/execution_testing/vm/opcodes.py b/packages/testing/src/execution_testing/vm/opcodes.py index f468bab77a6..56385afa7c5 100644 --- a/packages/testing/src/execution_testing/vm/opcodes.py +++ b/packages/testing/src/execution_testing/vm/opcodes.py @@ -5274,6 +5274,35 @@ class Opcodes(Opcode, Enum): Source: [evm.codes/#A4](https://www.evm.codes/#A4) """ + EXTENSION = Opcode(0xAE, terminating=True) + """ + EXTENSION() + ---- + + Description + ---- + Instruction reserved as an extension prefix for non-Ethereum-L1 EVM + chains; behaves exactly like INVALID on Ethereum L1 + + Inputs + ---- + None + + Outputs + ---- + None + + Fork + ---- + Bogota + + Gas + ---- + All the remaining gas in this context is consumed + + Source: [EIP-8163](https://eips.ethereum.org/EIPS/eip-8163) + """ + DUPN = Opcode( 0xE6, pushed_stack_items=1, diff --git a/tests/bogota/__init__.py b/tests/bogota/__init__.py new file mode 100644 index 00000000000..3fc98ba09f8 --- /dev/null +++ b/tests/bogota/__init__.py @@ -0,0 +1 @@ +"""Test cases for EVM functionality introduced in Bogota.""" diff --git a/tests/bogota/eip8163_extension/__init__.py b/tests/bogota/eip8163_extension/__init__.py new file mode 100644 index 00000000000..26602358264 --- /dev/null +++ b/tests/bogota/eip8163_extension/__init__.py @@ -0,0 +1 @@ +"""Tests for [EIP-8163: Reserve EXTENSION (0xae) opcode](https://eips.ethereum.org/EIPS/eip-8163).""" diff --git a/tests/bogota/eip8163_extension/spec.py b/tests/bogota/eip8163_extension/spec.py new file mode 100644 index 00000000000..2d26993b76b --- /dev/null +++ b/tests/bogota/eip8163_extension/spec.py @@ -0,0 +1,17 @@ +"""Reference spec for [EIP-8163: Reserve EXTENSION (0xae) opcode](https://eips.ethereum.org/EIPS/eip-8163).""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class ReferenceSpec: + """Reference specification.""" + + git_path: str + version: str + + +ref_spec_8163 = ReferenceSpec( + git_path="EIPS/eip-8163.md", + version="aef32ae28e42ba3a391ed8d991ddf6a68d53c152", +) diff --git a/tests/bogota/eip8163_extension/test_extension.py b/tests/bogota/eip8163_extension/test_extension.py new file mode 100644 index 00000000000..eea76fd2d22 --- /dev/null +++ b/tests/bogota/eip8163_extension/test_extension.py @@ -0,0 +1,195 @@ +""" +Tests for [EIP-8163: Reserve EXTENSION (0xae) opcode](https://eips.ethereum.org/EIPS/eip-8163). + +EXTENSION behaves exactly like INVALID on chains with no extensions +defined for it, Ethereum L1 included, and the byte is neutral to +JUMPDEST analysis everywhere. All assertions here hold on any +EIP-8163-conformant EVM, including chains that define extensions. + +These tests do not assert the non-existence of extensions that would +be conformant but are undefined today: there is no way to +forward-guess under what conditions such an extension would succeed, +and no point in testing it. +""" + +import pytest +from execution_testing import ( + Account, + Alloc, + Bytecode, + Fork, + Op, + StateTestFiller, + Transaction, + TransactionReceipt, +) + +from .spec import ref_spec_8163 + +REFERENCE_SPEC_GIT_PATH = ref_spec_8163.git_path +REFERENCE_SPEC_VERSION = ref_spec_8163.version + +pytestmark = pytest.mark.valid_from("EIP8163") + +slot_code_worked = 1 +value_code_worked = 0x1234 +value_code_untouched = 0xBA5E + + +@pytest.mark.parametrize( + "opcode,success,all_gas_consumed", + [ + pytest.param(Op.EXTENSION, False, True), + pytest.param(Op.INVALID, False, True), + pytest.param(Op.REVERT(0, 0), False, False, id="REVERT"), + pytest.param(Op.JUMPDEST, True, False), + ], +) +def test_top_level_call( + state_test: StateTestFiller, + pre: Alloc, + fork: Fork, + opcode: Bytecode, + success: bool, + all_gas_consumed: bool, +) -> None: + """ + Call a contract whose whole code is the single tested byte. + + EXTENSION behaves as INVALID, JUMPDEST as sanity check. + """ + contract_address = pre.deploy_contract(code=opcode) + + # One gas more than the code needs, so a normal halt refunds exactly + # that gas while an exceptional halt consumes it with the rest. + execution_gas = ( + fork.transaction_intrinsic_cost_calculator()() + opcode.gas_cost(fork) + ) + gas_limit = execution_gas + 1 + tx = Transaction( + gas_limit=gas_limit, + to=contract_address, + sender=pre.fund_eoa(), + expected_receipt=TransactionReceipt( + status=int(success), + cumulative_gas_used=( + gas_limit if all_gas_consumed else execution_gas + ), + ), + ) + + state_test(pre=pre, post={}, tx=tx) + + +@pytest.mark.parametrize( + "target,valid_jump", + [ + pytest.param(Op.JUMPDEST, True), + pytest.param(Op.PUSH1(0x5B), False, id="push_data_0x5b"), + ], +) +@pytest.mark.parametrize( + "following", + [ + pytest.param(Bytecode(), id="nothing"), + Op.STOP, + Op.ADD, + Op.JUMPDEST, + pytest.param(Op.PUSH1(0), id="PUSH1"), + pytest.param(Op.PUSH32(0), id="PUSH32"), + Op.EXTENSION, + Op.SELFDESTRUCT, + ], +) +def test_jumpdest_analysis_neutrality( + state_test: StateTestFiller, + pre: Alloc, + following: Bytecode, + target: Bytecode, + valid_jump: bool, +) -> None: + """ + Jump over an EXTENSION byte to a destination right behind it. + + JUMPDEST analysis ignores EXTENSION: a JUMPDEST behind EXTENSION + is a valid destination and a 0x5b held as PUSH1 data behind + EXTENSION is not. EXTENSION is never executed. Only JUMPDEST, the + PUSH opcodes and EXTENSION itself matter to the analysis of the + byte behind EXTENSION; every other byte is data to it. + """ + code = ( + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.JUMP(pc=Op.PUSH2(data_placeholder="destination")) + + Op.EXTENSION + + following + + target + ) + code.substitute(destination=len(code) - 1) + + contract_address = pre.deploy_contract( + code=code, + storage={slot_code_worked: value_code_untouched}, + ) + + tx = Transaction(to=contract_address, sender=pre.fund_eoa()) + + value = value_code_worked if valid_jump else value_code_untouched + state_test( + pre=pre, + post={contract_address: Account(storage={slot_code_worked: value})}, + tx=tx, + ) + + +@pytest.mark.parametrize( + "opcode,success", + [ + pytest.param(Op.EXTENSION, False), + pytest.param(Op.INVALID, False), + pytest.param(Op.JUMPDEST, True), + ], +) +@pytest.mark.parametrize( + "following", + [ + pytest.param(bytes([b]), id=f"0x{b:02x}") + for b in [0x5B, *range(0x60, 0x80)] + ] + + [pytest.param(b"", id="nothing")], +) +def test_solo_extension_bytes( + state_test: StateTestFiller, + pre: Alloc, + opcode: Op, + success: bool, + following: bytes, +) -> None: + """ + Execute the tested byte followed by a solo 0x5b or 0x60..0x7f + byte (or nothing), expect it to halt exceptionally always. + + EIP-8163 rules these out as single-byte extension immediates. + The stack is filled first, so the halt is known not to require an + empty stack. + + EXTENSION behaves as INVALID, JUMPDEST as sanity check. + """ + code = ( + Op.SSTORE(slot_code_worked, value_code_worked) + + Op.PUSH1(1) * 256 + + opcode + + following + ) + contract_address = pre.deploy_contract( + code=code, + storage={slot_code_worked: value_code_untouched}, + ) + + tx = Transaction(to=contract_address, sender=pre.fund_eoa()) + + value = value_code_worked if success else value_code_untouched + state_test( + pre=pre, + post={contract_address: Account(storage={slot_code_worked: value})}, + tx=tx, + )