Skip to content

Commit

Permalink
feat(cli): evm_bytes rename + print asm (#844)
Browse files Browse the repository at this point in the history
* feat(cli): evm_bytes print asm

* docs: changelog

* fix(cli): evm_bytes

* fix(cli): use commands in `evm_bytes`

* chore(evm_bytes): add google-style docstrings with examples

* docs(evm_bytes): add evm_bytes cli to docs

* fix(docs): tox

* fix(cli): tox

* docs(evm_bytes): improve sub-section titles

* chore(evm_bytes): use titles in example admonitions

---------

Co-authored-by: danceratopz <[email protected]>
  • Loading branch information
marioevz and danceratopz authored Oct 4, 2024
1 parent 7371ace commit 888fac5
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 75 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ Due to changes in the framework, there is a breaking change in the directory str
- 🔀 Filling tool: Updated the default filling tool (`t8n`) to go-ethereum@master ([#368](https://github.com/ethereum/execution-spec-tests/pull/368)).
- 🐞 Docs: Fix error banner in online docs due to mermaid syntax error ([#398](https://github.com/ethereum/execution-spec-tests/pull/398)).
- 🐞 Docs: Fix incorrectly formatted nested lists in online doc ([#403](https://github.com/ethereum/execution-spec-tests/pull/403)).
- 🔀 CLI: `evm_bytes_to_python` is renamed to `evm_bytes` and now accepts flag `--assembly` to output the code in assembly format ([#844](https://github.com/ethereum/execution-spec-tests/pull/844))

### 💥 Breaking Changes

Expand Down
20 changes: 20 additions & 0 deletions docs/library/cli/evm_bytes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The `evm_bytes` CLI

::: cli.evm_bytes.cli
options:
show_source: false
show_root_toc_entry: false

## `evm_bytes hex-string <bytecode>`

::: cli.evm_bytes.hex_string
options:
show_source: false
show_root_toc_entry: false

## `evm_bytes binary-file <contract.bin>`

::: cli.evm_bytes.binary_file
options:
show_source: false
show_root_toc_entry: false
3 changes: 3 additions & 0 deletions docs/library/cli/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# EEST CLI Tools

* [`evm_bytes`](evm_bytes.md) - Convert the given EVM bytes from a binary file or a hex string to EEST's python opcodes.
1 change: 1 addition & 0 deletions docs/navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
* [Running Github Actions Locally](dev/test_actions_locally.md)
* [Changelog](CHANGELOG.md)
* [Library Reference](library/index.md)
* [Miscellaneous CLI Tools](library/cli/index.md)
* [Ethereum Test Base Types Package](library/ethereum_test_base_types.md)
* [Ethereum Test Exceptions Package](library/ethereum_test_exceptions.md)
* [Ethereum Test Fixtures Package](library/ethereum_test_fixtures.md)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ gentest = "cli.gentest:make_test"
pyspelling_soft_fail = "cli.tox_helpers:pyspelling"
markdownlintcli2_soft_fail = "cli.tox_helpers:markdownlint"
order_fixtures = "cli.order_fixtures:order_fixtures"
evm_bytes_to_python = "cli.evm_bytes_to_python:main"
evm_bytes = "cli.evm_bytes:cli"
hasher = "cli.hasher:main"

[tool.setuptools.packages.find]
Expand Down
212 changes: 212 additions & 0 deletions src/cli/evm_bytes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""
Define an entry point wrapper for pytest.
"""

from dataclasses import dataclass, field
from typing import List

import click

from ethereum_test_base_types import ZeroPaddedHexNumber
from ethereum_test_vm import Macro
from ethereum_test_vm import Opcodes as Op

OPCODES_WITH_EMPTY_LINES_AFTER = {
Op.STOP,
Op.REVERT,
Op.INVALID,
Op.JUMP,
Op.JUMPI,
}

OPCODES_WITH_EMPTY_LINES_BEFORE = {
Op.JUMPDEST,
}


@dataclass(kw_only=True)
class OpcodeWithOperands:
"""Simple opcode with its operands."""

opcode: Op | None
operands: List[int] = field(default_factory=list)

def format(self, assembly: bool) -> str:
"""Format the opcode with its operands."""
if self.opcode is None:
return ""
if assembly:
return self.format_assembly()
if self.operands:
operands = ", ".join(hex(operand) for operand in self.operands)
return f"Op.{self.opcode._name_}[{operands}]"
return f"Op.{self.opcode._name_}"

def format_assembly(self) -> str:
"""Format the opcode with its operands as assembly."""
if self.opcode is None:
return ""
opcode_name = self.opcode._name_.lower()
if self.opcode.data_portion_length == 0:
return f"{opcode_name}"
elif self.opcode == Op.RJUMPV:
operands = ", ".join(str(ZeroPaddedHexNumber(operand)) for operand in self.operands)
return f"{opcode_name} {operands}"
else:
operands = ", ".join(str(ZeroPaddedHexNumber(operand)) for operand in self.operands)
return f"{opcode_name} {operands}"


def process_evm_bytes(evm_bytes: bytes, assembly: bool = False) -> str: # noqa: D103
evm_bytes = bytearray(evm_bytes)

opcodes: List[OpcodeWithOperands] = []

while evm_bytes:
opcode_byte = evm_bytes.pop(0)

opcode: Op
for op in Op:
if not isinstance(op, Macro) and op.int() == opcode_byte:
opcode = op
break
else:
raise ValueError(f"Unknown opcode: {opcode_byte}")

if opcode.data_portion_length > 0:
opcodes.append(
OpcodeWithOperands(
opcode=opcode,
operands=[int.from_bytes(evm_bytes[: opcode.data_portion_length], "big")],
)
)
evm_bytes = evm_bytes[opcode.data_portion_length :]
elif opcode == Op.RJUMPV:
max_index = evm_bytes.pop(0)
operands: List[int] = []
for _ in range(max_index + 1):
operands.append(int.from_bytes(evm_bytes[:2], "big"))
evm_bytes = evm_bytes[2:]
opcodes.append(OpcodeWithOperands(opcode=opcode, operands=operands))
else:
opcodes.append(OpcodeWithOperands(opcode=opcode))

if assembly:
opcodes_with_empty_lines: List[OpcodeWithOperands] = []
for i, op_with_operands in enumerate(opcodes):
if (
op_with_operands.opcode in OPCODES_WITH_EMPTY_LINES_BEFORE
and len(opcodes_with_empty_lines) > 0
and opcodes_with_empty_lines[-1].opcode is not None
):
opcodes_with_empty_lines.append(OpcodeWithOperands(opcode=None))
opcodes_with_empty_lines.append(op_with_operands)
if op_with_operands.opcode in OPCODES_WITH_EMPTY_LINES_AFTER and i < len(opcodes) - 1:
opcodes_with_empty_lines.append(OpcodeWithOperands(opcode=None))
return "\n".join(op.format(assembly) for op in opcodes_with_empty_lines)
return " + ".join(op.format(assembly) for op in opcodes)


def process_evm_bytes_string(evm_bytes_hex_string: str, assembly: bool = False) -> str:
"""Process the given EVM bytes hex string."""
if evm_bytes_hex_string.startswith("0x"):
evm_bytes_hex_string = evm_bytes_hex_string[2:]

evm_bytes = bytes.fromhex(evm_bytes_hex_string)
return process_evm_bytes(evm_bytes, assembly=assembly)


assembly_option = click.option(
"-a",
"--assembly",
default=False,
is_flag=True,
help="Output the code as assembly instead of python.",
)


@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
def cli():
"""
Convert the given EVM bytes to EEST's python opcodes or assembly string.
The input can be either a hex string or a binary file containing EVM bytes.
"""
pass


@cli.command()
@assembly_option
@click.argument("hex_string")
def hex_string(hex_string: str, assembly: bool):
"""
Process a hex string representing EVM bytes and convert it into EEST's Python opcodes.
Args:
hex_string (str): The hex string representing the EVM bytes.
assembly (bool): Whether to print the output as assembly or Python opcodes.
Returns:
(str): The processed EVM opcodes in Python or assembly format.
Example: Convert a hex string to EEST Python `Opcodes`
```bash
uv run evm_bytes hex-string 604260005260206000F3
```
Output:
```python
Op.PUSH1[0x42] + Op.PUSH1[0x0] + Op.MSTORE + Op.PUSH1[0x20] + Op.PUSH1[0x0] + Op.RETURN
```
Example: Convert a hex string to assembly
```bash
uv run evm_bytes hex-string --assembly 604260005260206000F3
```
Output:
```text
push1 0x42
push1 0x00
mstore
push1 0x20
push1 0x00
return
```
""" # noqa: E501
processed_output = process_evm_bytes_string(hex_string, assembly=assembly)
click.echo(processed_output)


@cli.command()
@assembly_option
@click.argument("binary_file_path", type=click.File("rb"))
def binary_file(binary_file_path, assembly: bool):
"""
Convert the given EVM bytes binary file.
Args:
binary_file_path (BinaryIO): A binary file containing EVM bytes to be processed or use `-`
to read from stdin.
assembly (bool): Whether to print the output as assembly or Python opcodes.
Example: Convert the Withdrawal Request contract to assembly
```bash
uv run evm_bytes binary-file ./src/ethereum_test_forks/forks/contracts/withdrawal_request.bin --assembly
```
Output:
```text
caller
push20 0xfffffffffffffffffffffffffffffffffffffffe
eq
push1 0x90
jumpi
...
```
""" # noqa: E501
processed_output = process_evm_bytes(binary_file_path.read(), assembly=assembly)
click.echo(processed_output)
64 changes: 0 additions & 64 deletions src/cli/evm_bytes_to_python.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""
Test suite for `cli.evm_bytes_to_python` module.
Test suite for `cli.evm_bytes` module.
"""

import pytest

from ethereum_test_tools import Opcodes as Op

from ..evm_bytes_to_python import process_evm_bytes
from ..evm_bytes import process_evm_bytes_string

basic_vector = [
"0x60008080808061AAAA612d5ff1600055",
Expand All @@ -27,9 +27,9 @@
(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
def test_evm_bytes(evm_bytes: str, python_opcodes: str):
"""Test evm_bytes using the basic and complex vectors"""
assert process_evm_bytes_string(evm_bytes) == python_opcodes


DUPLICATES = [Op.NOOP]
Expand All @@ -40,7 +40,7 @@ def test_evm_bytes_to_python(evm_bytes, python_opcodes):
[op for op in Op if op not in DUPLICATES],
ids=lambda op: op._name_,
)
def test_individual_opcodes(opcode):
def test_individual_opcodes(opcode: Op):
"""Test each opcode individually"""
data_portion = b""
if opcode.data_portion_length > 0:
Expand All @@ -53,17 +53,17 @@ def test_individual_opcodes(opcode):
expected_output = f"Op.{opcode._name_}"

bytecode = opcode.int().to_bytes(1, byteorder="big") + data_portion
assert process_evm_bytes("0x" + bytecode.hex()) == expected_output
assert process_evm_bytes_string("0x" + bytecode.hex()) == expected_output


def test_invalid_opcode():
"""Invalid hex string"""
with pytest.raises(ValueError):
process_evm_bytes("0xZZ")
process_evm_bytes_string("0xZZ")


def test_unknown_opcode():
"""Opcode not defined in Op"""
with pytest.raises(ValueError):
process_evm_bytes("0x0F")
process_evm_bytes("0x0F")
process_evm_bytes_string("0x0F")
process_evm_bytes_string("0x0F")
Loading

0 comments on commit 888fac5

Please sign in to comment.