-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli):
evm_bytes
rename + print asm (#844)
* 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
1 parent
7371ace
commit 888fac5
Showing
9 changed files
with
250 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.