diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b87773ea71..c73566abb6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,7 @@ Test fixtures for use by clients are available for each release on the [Github r - ✨ Add a `--single-fixture-per-file` flag to generate one fixture JSON file per test case ([#331](https://github.com/ethereum/execution-spec-tests/pull/331)). - 🔀 Rename test fixtures names to match the corresponding pytest node ID as generated using `fill` ([#342](https://github.com/ethereum/execution-spec-tests/pull/342)). - 💥 Replace "=" with "_" in pytest node ids and test fixture names ([#342](https://github.com/ethereum/execution-spec-tests/pull/342)). +- ✨ Add `evm_bytes_to_python` command which converts EVM bytes to Python Opcodes ([#357](https://github.com/ethereum/execution-spec-tests/pull/357)) - 🔀 Locally calculate the transactions list's root instead of using the one returned by t8n when producing BlockchainTests ([#353](https://github.com/ethereum/execution-spec-tests/pull/353)) - ✨ Fork objects used to write tests can now be compared using the `>`, `>=`, `<`, `<=` operators, to check for a fork being newer than, newer than or equal, older than, older than or equal, respectively when compared against other fork ([#367](https://github.com/ethereum/execution-spec-tests/pull/367)) - 🐞 Storage type iterator is now fixed ([#369](https://github.com/ethereum/execution-spec-tests/pull/369)) diff --git a/setup.cfg b/setup.cfg index 91b2764489..1ba8bde67a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ console_scripts = pyspelling_soft_fail = entry_points.pyspelling_soft_fail:main markdownlintcli2_soft_fail = entry_points.markdownlintcli2_soft_fail:main create_whitelist_for_flake8_spelling = entry_points.create_whitelist_for_flake8_spelling:main + evm_bytes_to_python = entry_points.evm_bytes_to_python:main [options.extras_require] test = diff --git a/src/entry_points/evm_bytes_to_python.py b/src/entry_points/evm_bytes_to_python.py new file mode 100644 index 0000000000..930c1bc1df --- /dev/null +++ b/src/entry_points/evm_bytes_to_python.py @@ -0,0 +1,57 @@ +""" +Define an entry point wrapper for pytest. +""" + +import sys +from typing import Any, List, Optional + +from ethereum_test_tools import Opcodes as Op + + +def process_evm_bytes(evm_bytes_hex_string: Any) -> str: # noqa: D103 + if evm_bytes_hex_string.startswith("0x"): + evm_bytes_hex_string = evm_bytes_hex_string[2:] + + evm_bytes = bytearray(bytes.fromhex(evm_bytes_hex_string)) + + opcodes_strings: List[str] = [] + + while evm_bytes: + opcode_byte = evm_bytes.pop(0) + + opcode: Optional[Op] = None + for op in Op: + if op.int() == opcode_byte: + opcode = op + break + + if opcode is None: + raise ValueError(f"Unknown opcode: {opcode_byte}") + + if opcode.data_portion_length > 0: + data_portion = evm_bytes[: opcode.data_portion_length] + evm_bytes = evm_bytes[opcode.data_portion_length :] + opcodes_strings.append(f'Op.{opcode._name_}("0x{data_portion.hex()}")') + else: + opcodes_strings.append(f"Op.{opcode._name_}") + + return " + ".join(opcodes_strings) + + +def print_help(): # noqa: D103 + print("Usage: evm_bytes_to_python ") + + +def main(): # noqa: D103 + if len(sys.argv) != 2: + print_help() + sys.exit(1) + if sys.argv[1] in ["-h", "--help"]: + print_help() + sys.exit(0) + evm_bytes_hex_string = sys.argv[1] + print(process_evm_bytes(evm_bytes_hex_string)) + + +if __name__ == "__main__": + main() diff --git a/src/entry_points/tests/__init__.py b/src/entry_points/tests/__init__.py new file mode 100644 index 0000000000..6a7a6059f9 --- /dev/null +++ b/src/entry_points/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Basic pytest applications `entry_points` unit tests. +""" diff --git a/src/entry_points/tests/test_evm_bytes_to_python.py b/src/entry_points/tests/test_evm_bytes_to_python.py new file mode 100644 index 0000000000..df2dd7f2dc --- /dev/null +++ b/src/entry_points/tests/test_evm_bytes_to_python.py @@ -0,0 +1,55 @@ +""" +Test suite for `entry_points.evm_bytes_to_python` module. +""" + +import pytest +from evm_bytes_to_python import process_evm_bytes + +from ethereum_test_tools import Opcodes as Op + +basic_vector = [ + "0x60008080808061AAAA612d5ff1600055", + 'Op.PUSH1("0x00") + Op.DUP1 + Op.DUP1 + Op.DUP1 + Op.DUP1 + Op.PUSH2("0xaaaa") + Op.PUSH2("0x2d5f") + Op.CALL + Op.PUSH1("0x00") + Op.SSTORE', # noqa: E501 +] +complex_vector = [ + "0x7fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf5f527fc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf6020527fe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff60405260786040356020355f35608a565b5f515f55602051600155604051600255005b5e56", # noqa: E501 + 'Op.PUSH32("0xa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf") + Op.PUSH0 + Op.MSTORE + Op.PUSH32("0xc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf") + Op.PUSH1("0x20") + Op.MSTORE + Op.PUSH32("0xe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff") + Op.PUSH1("0x40") + Op.MSTORE + Op.PUSH1("0x78") + Op.PUSH1("0x40") + Op.CALLDATALOAD + Op.PUSH1("0x20") + Op.CALLDATALOAD + Op.PUSH0 + Op.CALLDATALOAD + Op.PUSH1("0x8a") + Op.JUMP + Op.JUMPDEST + Op.PUSH0 + Op.MLOAD + Op.PUSH0 + Op.SSTORE + Op.PUSH1("0x20") + Op.MLOAD + Op.PUSH1("0x01") + Op.SSTORE + Op.PUSH1("0x40") + Op.MLOAD + Op.PUSH1("0x02") + Op.SSTORE + Op.STOP + Op.JUMPDEST + Op.MCOPY + Op.JUMP', # noqa: E501 +] + + +@pytest.mark.parametrize( + "evm_bytes, python_opcodes", + [ + (basic_vector[0], basic_vector[1]), + (basic_vector[0][2:], basic_vector[1]), # no "0x" prefix + (complex_vector[0], complex_vector[1]), + (complex_vector[0][2:], complex_vector[1]), # no "0x" prefix + ], +) +def test_evm_bytes_to_python(evm_bytes, python_opcodes): + """Test evm_bytes_to_python using the basic and complex vectors""" + assert process_evm_bytes(evm_bytes) == python_opcodes + + +@pytest.mark.parametrize("opcode", list(Op)) +def test_individual_opcodes(opcode): + """Test each opcode individually""" + if opcode.data_portion_length > 0: + expected_output = f'Op.{opcode._name_}("0x")' + else: + expected_output = f"Op.{opcode._name_}" + + bytecode = opcode.int().to_bytes(1, byteorder="big").hex() + assert process_evm_bytes("0x" + bytecode) == expected_output + + +def test_invalid_opcode(): + """Invalid hex string""" + with pytest.raises(ValueError): + process_evm_bytes("0xZZ") + + +def test_unknown_opcode(): + """Opcode not defined in Op""" + with pytest.raises(ValueError): + process_evm_bytes("0x0F")