From 45da1604d2a7ce01df42535467c9a2026fcff0f1 Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 8 Jun 2022 12:48:32 -0500 Subject: [PATCH 1/4] feat: evm display trace --- .pre-commit-config.yaml | 10 +- README.md | 48 ++++++++- evm_trace/base.py | 60 +++++++----- evm_trace/display.py | 76 ++++++++++++++ evm_trace/enums.py | 9 ++ pyproject.toml | 3 +- setup.py | 9 +- tests/{.gitkeep => __init__.py} | 0 tests/conftest.py | 169 ++++++++++++++++++++++++++++++++ tests/test_call_tree.py | 30 ++++++ tests/test_trace_frame.py | 50 +++------- 11 files changed, 389 insertions(+), 75 deletions(-) create mode 100644 evm_trace/display.py create mode 100644 evm_trace/enums.py rename tests/{.gitkeep => __init__.py} (100%) create mode 100644 tests/conftest.py create mode 100644 tests/test_call_tree.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5903073..d7a848a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,27 +1,27 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.2.0 hooks: - id: check-yaml - repo: https://github.com/pre-commit/mirrors-isort - rev: v5.9.3 + rev: v5.10.1 hooks: - id: isort - repo: https://github.com/psf/black - rev: 21.10b0 + rev: 22.3.0 hooks: - id: black name: black - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.2 + rev: 4.0.1 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.910-1 + rev: v0.960 hooks: - id: mypy additional_dependencies: [types-PyYAML, types-requests] diff --git a/README.md b/README.md index dd251dd..cd0bfea 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,52 @@ python3 setup.py install ``` ## Quick Usage -```bash -ape console --network ethereum:local:hardhat + +If you are using a node that supports the `debug_traceTransaction` RPC, you can use `web3.py` to get trace frames: + +```python +from web3 import HTTPProvider, Web3 +from evm_trace import TraceFrame + +web3 = Web3(HTTPProvider("https://path.to.my.node")) +struct_logs = web3.manager.request_blocking("debug_traceTransaction", [txn_hash]).structLogs +for item in struct_logs: + yield TraceFrame(**item) +``` + +If you want to get the call-tree node, you can do: + +```python +from evm_trace import CallType, get_calltree_from_trace + +root_node_kwargs = { + "gas_cost": 10000000, + "gas_limit": 10000000000, + "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "calldata": "0x00", + "value": 1000, + "call_type": CallType.MUTABLE, +} + +# Where `trace` is a `TraceFrame` (see example above) +calltree = get_calltree_from_trace(trace, **root_node_kwargs) +``` + +You can also customize the output by making your own display class: + +```python +from evm_trace.display import DisplayableCallTreeNode, get_calltree_from_trace + + +class CustomDisplay(DisplayableCallTreeNode): + def title(self) -> str: + call_type = self.call.call_type.value.lower().capitalize() + address = self.call.address.hex() + cost = self.call.gas_cost + return f"{call_type} call @ {address} gas_cost={cost}" + + +calltree = get_calltree_from_trace(trace, display_cls=CustomDisplay) ``` ## Development diff --git a/evm_trace/base.py b/evm_trace/base.py index 80fa0d1..8ac956d 100644 --- a/evm_trace/base.py +++ b/evm_trace/base.py @@ -1,11 +1,13 @@ import math -from enum import Enum -from typing import Any, Dict, Iterator, List +from typing import Any, Dict, Iterable, Iterator, List, Type from eth_utils import to_int from hexbytes import HexBytes from pydantic import BaseModel, Field, ValidationError, validator +from evm_trace.display import DisplayableCallTreeNode +from evm_trace.enums import CallType + def _convert_hexbytes(cls, v: Any) -> HexBytes: try: @@ -33,14 +35,6 @@ def convert_hexbytes_dict(cls, v) -> Dict[HexBytes, HexBytes]: return {_convert_hexbytes(cls, k): _convert_hexbytes(cls, val) for k, val in v.items()} -class CallType(Enum): - INTERNAL = "INTERNAL" # Non-opcode internal call - STATIC = "STATIC" # STATICCALL opcode - MUTABLE = "MUTABLE" # CALL opcode - DELEGATE = "DELEGATE" # DELEGATECALL opcode - SELFDESTRUCT = "SELFDESTRUCT" # SELFDESTRUCT opcode - - class CallTreeNode(BaseModel): call_type: CallType address: Any @@ -53,11 +47,25 @@ class CallTreeNode(BaseModel): calls: List["CallTreeNode"] = [] selfdestruct: bool = False failed: bool = False + display_cls: Type[DisplayableCallTreeNode] = DisplayableCallTreeNode + + @property + def display_nodes(self) -> Iterable[DisplayableCallTreeNode]: + return self.display_cls.make_tree(self) @validator("address", "calldata", "returndata", pre=True) def validate_hexbytes(cls, v) -> HexBytes: return _convert_hexbytes(cls, v) + def __str__(self) -> str: + return "\n".join([str(t) for t in self.display_nodes]) + + def __repr__(self) -> str: + return str(self) + + def __getitem__(self, index: int) -> "CallTreeNode": + return self.calls[index] + def get_calltree_from_trace( trace: Iterator[TraceFrame], show_internal=False, **root_node_kwargs @@ -68,17 +76,18 @@ def get_calltree_from_trace( Args: trace (Iterator[TraceFrame]): Iterator of transaction trace frames. show_internal (bool): Boolean whether to display internal calls. Defaulted to False. - root_node_kwargs (dict): Keyword argments passed to the root ``CallTreeNode``. + root_node_kwargs (dict): Keyword arguments passed to the root ``CallTreeNode``. Returns: - :class:`~evm_trace.base.CallTreeNode: Call tree of transaction trace. + :class:`~evm_trace.base.CallTreeNode`: Call tree of transaction trace. """ - return _create_node_from_call( + node = _create_node_from_call( trace=trace, show_internal=show_internal, **root_node_kwargs, ) + return node def _extract_memory(offset: HexBytes, size: HexBytes, memory: List[HexBytes]) -> HexBytes: @@ -106,13 +115,15 @@ def _extract_memory(offset: HexBytes, size: HexBytes, memory: List[HexBytes]) -> # Compute the word that contains the last byte stop_word = math.ceil((offset_int + size_int) / 32) - byte_slice = b"" + end_index = stop_word + 1 + byte_slice = b"".join(memory[start_word:end_index]) + offset_index = offset_int % 32 - for word in memory[start_word:stop_word]: - byte_slice += word + # NOTE: Add 4 for the selector. - offset_index = offset_int % 32 - return HexBytes(byte_slice[offset_index:size_int]) + end_bytes_index = offset_index + size_int + return_bytes = byte_slice[offset_index:end_bytes_index] + return HexBytes(return_bytes) def _create_node_from_call( @@ -127,16 +138,16 @@ def _create_node_from_call( raise NotImplementedError() node = CallTreeNode(**node_kwargs) - for frame in trace: if frame.op in ("CALL", "DELEGATECALL", "STATICCALL"): - child_node_kwargs = {} + child_node_kwargs = { + "address": frame.stack[-2][-20:], + "depth": frame.depth, + "gas_limit": int(frame.stack[-1].hex(), 16), + "gas_cost": frame.gas_cost, + } - child_node_kwargs["address"] = frame.stack[-2][-20:] # address is 20 bytes in EVM - child_node_kwargs["depth"] = frame.depth # TODO: Validate gas values - child_node_kwargs["gas_limit"] = int(frame.stack[-1].hex(), 16) - child_node_kwargs["gas_cost"] = frame.gas_cost if frame.op == "CALL": child_node_kwargs["call_type"] = CallType.MUTABLE @@ -178,4 +189,5 @@ def _create_node_from_call( # NOTE: ignore other opcodes # TODO: Handle "execution halted" vs. gas limit reached + return node diff --git a/evm_trace/display.py b/evm_trace/display.py new file mode 100644 index 0000000..b6f80da --- /dev/null +++ b/evm_trace/display.py @@ -0,0 +1,76 @@ +from typing import TYPE_CHECKING, Iterable, Optional + +from eth_utils import to_checksum_address + +from evm_trace.enums import CallType + +if TYPE_CHECKING: + from evm_trace.base import CallTreeNode + + +class DisplayableCallTreeNode(object): + _FILE_MIDDLE_PREFIX = "├──" + _FILE_LAST_PREFIX = "└──" + _PARENT_PREFIX_MIDDLE = " " + _PARENT_PREFIX_LAST = "│ " + + def __init__( + self, + call: "CallTreeNode", + parent: Optional["DisplayableCallTreeNode"] = None, + is_last: bool = False, + ): + self.call = call + self.parent = parent + self.is_last = is_last + + @property + def depth(self) -> int: + return self.call.depth + + @property + def title(self) -> str: + call_type = self.call.call_type.value.upper() + call_mnemonic = "CALL" if self.call.call_type == CallType.MUTABLE else f"{call_type}CALL" + address = to_checksum_address(self.call.address.hex()) + cost = self.call.gas_cost + + call_path = str(address) + if self.call.calldata: + call_path = f"{call_path}.<{self.call.calldata[:4].hex()}>" + + return f"{call_mnemonic}: {call_path} [{cost} gas]" + + @classmethod + def make_tree( + cls, + root: "CallTreeNode", + parent: Optional["DisplayableCallTreeNode"] = None, + is_last: bool = False, + ) -> Iterable["DisplayableCallTreeNode"]: + displayable_root = cls(root, parent=parent, is_last=is_last) + yield displayable_root + + count = 1 + for child_node in root.calls: + is_last = count == len(root.calls) + if child_node.calls: + yield from cls.make_tree(child_node, parent=displayable_root, is_last=is_last) + else: + yield cls(child_node, parent=displayable_root, is_last=is_last) + + count += 1 + + def __str__(self) -> str: + if self.parent is None: + return self.title + + _filename_prefix = self._FILE_LAST_PREFIX if self.is_last else self._FILE_MIDDLE_PREFIX + + parts = [f"{_filename_prefix} {self.title}"] + parent = self.parent + while parent and parent.parent is not None: + parts.append(self._PARENT_PREFIX_MIDDLE if parent.is_last else self._PARENT_PREFIX_LAST) + parent = parent.parent + + return "".join(reversed(parts)) diff --git a/evm_trace/enums.py b/evm_trace/enums.py new file mode 100644 index 0000000..f960a9a --- /dev/null +++ b/evm_trace/enums.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class CallType(Enum): + INTERNAL = "INTERNAL" # Non-opcode internal call + STATIC = "STATIC" # STATICCALL opcode + MUTABLE = "MUTABLE" # CALL opcode + DELEGATE = "DELEGATE" # DELEGATECALL opcode + SELFDESTRUCT = "SELFDESTRUCT" # SELFDESTRUCT opcode diff --git a/pyproject.toml b/pyproject.toml index f2ce3dc..d360f98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,12 +14,11 @@ write_to = "evm_trace/version.py" [tool.black] line-length = 100 -target-version = ['py37', 'py38', 'py39'] +target-version = ['py37', 'py38', 'py39', 'py310'] include = '\.pyi?$' [tool.pytest.ini_options] addopts = """ - -n auto -p no:ape_test --cov-branch --cov-report term diff --git a/setup.py b/setup.py index c24c1a5..ec9c4d8 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,9 @@ ], "lint": [ "black>=22.3.0,<23.0", # auto-formatter and linter - "mypy>=0.910,<1.0", # Static type analyzer - "flake8>=3.8.3,<4.0", # Style linter - "isort>=5.9.3,<6.0", # Import sorting linter + "mypy>=0.960,<1.0", # Static type analyzer + "flake8>=4.0.1,<5.0", # Style linter + "isort>=5.10.1,<6.0", # Import sorting linter ], "release": [ # `release` GitHub Action job uses this "setuptools", # Installation tool @@ -56,7 +56,7 @@ "importlib-metadata ; python_version<'3.8'", "pydantic>=1.9.0,<2.0", "hexbytes>=0.2.2,<1.0.0", - "eth-utils==1.10.0", + "eth-utils>=1.10.0", ], # NOTE: Add 3rd party libraries here python_requires=">=3.7.2,<4", extras_require=extras_require, @@ -77,5 +77,6 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], ) diff --git a/tests/.gitkeep b/tests/__init__.py similarity index 100% rename from tests/.gitkeep rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..baf192e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,169 @@ +import pytest +from hexbytes import HexBytes + +from evm_trace.base import CallType + +TRACE_FRAME_STRUCTURE = { + "pc": 1564, + "op": "RETURN", + "gas": 0, + "gasCost": 0, + "depth": 1, + "callType": CallType.MUTABLE.value, + "stack": [ + "0000000000000000000000000000000000000000000000000000000040c10f19", + "0000000000000000000000000000000000000000000000000000000000000020", + "0000000000000000000000000000000000000000000000000000000000000140", + ], + "memory": [ + "0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", + "0000000000000000000000000000000000000000000000000000000000000001", + ], + "storage": { + "0000000000000000000000000000000000000000000000000000000000000004": "0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", # noqa: E501 + "ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5": "0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", # noqa: E501 + "aadb61a4b4c5d48b7a5669391b7c73852a3ab7795f24721b9a439220b54b591b": "0000000000000000000000000000000000000000000000000000000000000001", # noqa: E501 + }, +} +CALL_TREE_STRUCTURE = { + "call_type": CallType.MUTABLE, + "address": HexBytes("0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488"), + "value": 0, + "depth": 0, + "gas_limit": 197110, + "gas_cost": 194827, + "calldata": HexBytes("0x"), + "returndata": HexBytes("0x"), + "calls": [ + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0xbcf7fffd8b256ec51a36782a52d0c34f6474d951"), + "value": 0, + "depth": 1, + "gas_limit": 171094, + "gas_cost": 168423, + "calldata": HexBytes("0x61d22ffe"), + "returndata": HexBytes("0x"), + "calls": [ + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0x274b028b03a250ca03644e6c578d81f019ee1323"), + "value": 0, + "depth": 2, + "gas_limit": 163393, + "gas_cost": 160842, + "calldata": HexBytes("0x8f27163e"), + "returndata": HexBytes("0x"), + "calls": [], + "selfdestruct": False, + "failed": False, + "displays_last": True, + } + ], + "selfdestruct": False, + "failed": False, + "displays_last": False, + }, + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0xbcf7fffd8b256ec51a36782a52d0c34f6474d951"), + "value": 0, + "depth": 1, + "gas_limit": 118796, + "gas_cost": 116942, + "calldata": HexBytes("0x61d22ffe"), + "returndata": HexBytes("0x"), + "calls": [ + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0x274b028b03a250ca03644e6c578d81f019ee1323"), + "value": 0, + "depth": 2, + "gas_limit": 116412, + "gas_cost": 114595, + "calldata": HexBytes("0x8f27163e"), + "returndata": HexBytes("0x"), + "calls": [], + "selfdestruct": False, + "failed": False, + "displays_last": True, + } + ], + "selfdestruct": False, + "failed": False, + "displays_last": False, + }, + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0xbcf7fffd8b256ec51a36782a52d0c34f6474d951"), + "value": 0, + "depth": 1, + "gas_limit": 94902, + "gas_cost": 93421, + "calldata": HexBytes( + "0xb9e5b20a0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c" + ), + "returndata": HexBytes("0x"), + "calls": [ + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0x274b028b03a250ca03644e6c578d81f019ee1323"), + "value": 0, + "depth": 2, + "gas_limit": 92724, + "gas_cost": 91277, + "calldata": HexBytes("0x8f27163e"), + "returndata": HexBytes("0x"), + "calls": [], + "selfdestruct": False, + "failed": False, + "displays_last": False, + }, + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0x274b028b03a250ca03644e6c578d81f019ee1323"), + "value": 0, + "depth": 2, + "gas_limit": 47212, + "gas_cost": 46476, + "calldata": HexBytes("0x90bb7141"), + "returndata": HexBytes("0x"), + "calls": [], + "selfdestruct": False, + "failed": False, + "displays_last": False, + }, + { + "call_type": CallType.MUTABLE, + "address": HexBytes("0x274b028b03a250ca03644e6c578d81f019ee1323"), + "value": 0, + "depth": 2, + "gas_limit": 23862, + "gas_cost": 23491, + "calldata": HexBytes("0x90bb7141"), + "returndata": HexBytes("0x"), + "calls": [], + "selfdestruct": False, + "failed": False, + "displays_last": True, + }, + ], + "selfdestruct": False, + "failed": False, + "displays_last": True, + }, + ], + "selfdestruct": False, + "failed": False, + "displays_last": True, +} + + +@pytest.fixture(scope="session") +def trace_frame_structure(): + return TRACE_FRAME_STRUCTURE + + +@pytest.fixture(scope="session") +def call_tree_structure(): + return CALL_TREE_STRUCTURE diff --git a/tests/test_call_tree.py b/tests/test_call_tree.py new file mode 100644 index 0000000..3ed3bd9 --- /dev/null +++ b/tests/test_call_tree.py @@ -0,0 +1,30 @@ +import pytest + +from evm_trace import CallTreeNode + +EXPECTED_TREE_REPR = """ +CALL: 0xF2Df0b975c0C9eFa2f8CA0491C2d1685104d2488 [194827 gas] +├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0x61d22ffe> [168423 gas] +│ └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x8f27163e> [160842 gas] +├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0x61d22ffe> [116942 gas] +│ └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x8f27163e> [114595 gas] +└── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0xb9e5b20a> [93421 gas] + ├── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x8f27163e> [91277 gas] + ├── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x90bb7141> [46476 gas] + └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x90bb7141> [23491 gas] +""".strip() + + +@pytest.fixture(scope="session") +def call_tree(call_tree_structure): + return CallTreeNode(**call_tree_structure) + + +def test_call_tree_validation_passes(call_tree_structure): + tree = CallTreeNode(**call_tree_structure) + assert tree + + +def test_call_tree_representation(call_tree): + actual = repr(call_tree) + assert actual == EXPECTED_TREE_REPR diff --git a/tests/test_trace_frame.py b/tests/test_trace_frame.py index 10de3bd..13b0ffb 100644 --- a/tests/test_trace_frame.py +++ b/tests/test_trace_frame.py @@ -1,49 +1,23 @@ -from copy import deepcopy - import pytest from pydantic import ValidationError from evm_trace.base import TraceFrame -TRACE_FRAME_STRUCTURE = { - "pc": 1564, - "op": "RETURN", - "gas": 0, - "gasCost": 0, - "depth": 1, - "stack": [ - "0000000000000000000000000000000000000000000000000000000040c10f19", - "0000000000000000000000000000000000000000000000000000000000000020", - "0000000000000000000000000000000000000000000000000000000000000140", - ], - "memory": [ - "0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", - "0000000000000000000000000000000000000000000000000000000000000001", - ], - "storage": { - "0000000000000000000000000000000000000000000000000000000000000004": "0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", # noqa: E501 - "ad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5": "0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", # noqa: E501 - "aadb61a4b4c5d48b7a5669391b7c73852a3ab7795f24721b9a439220b54b591b": "0000000000000000000000000000000000000000000000000000000000000001", # noqa: E501 - }, -} - -def test_trace_frame_validation_passes(): - frame = TraceFrame(**TRACE_FRAME_STRUCTURE) +def test_trace_frame_validation_passes(trace_frame_structure): + frame = TraceFrame(**trace_frame_structure) assert frame -trace_frame_test_cases = ( - {"stack": ["potato"]}, - {"memory": ["potato"]}, - {"storage": {"piggy": "dippin"}}, +@pytest.mark.parametrize( + "test_data", + ( + {"stack": ["potato"]}, + {"memory": ["potato"]}, + {"storage": {"piggy": "dippin"}}, + ), ) - - -@pytest.mark.parametrize("test_value", trace_frame_test_cases) -def test_trace_frame_validation_fails(test_value): - trace_frame_structure = deepcopy(TRACE_FRAME_STRUCTURE) - trace_frame_structure.update(test_value) - +def test_trace_frame_validation_fails(test_data, trace_frame_structure): + data = {**trace_frame_structure, **test_data} with pytest.raises(ValidationError): - TraceFrame(**trace_frame_structure) + TraceFrame(**data) From 7feea0dc05ceb8b33721c376ca62fa801a1256cd Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 8 Jun 2022 14:36:23 -0500 Subject: [PATCH 2/4] test: structure -> data --- tests/conftest.py | 12 ++++++------ tests/test_call_tree.py | 8 ++++---- tests/test_trace_frame.py | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index baf192e..86f87d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from evm_trace.base import CallType -TRACE_FRAME_STRUCTURE = { +TRACE_FRAME_DATA = { "pc": 1564, "op": "RETURN", "gas": 0, @@ -25,7 +25,7 @@ "aadb61a4b4c5d48b7a5669391b7c73852a3ab7795f24721b9a439220b54b591b": "0000000000000000000000000000000000000000000000000000000000000001", # noqa: E501 }, } -CALL_TREE_STRUCTURE = { +CALL_TREE_DATA = { "call_type": CallType.MUTABLE, "address": HexBytes("0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488"), "value": 0, @@ -160,10 +160,10 @@ @pytest.fixture(scope="session") -def trace_frame_structure(): - return TRACE_FRAME_STRUCTURE +def trace_frame_data(): + return TRACE_FRAME_DATA @pytest.fixture(scope="session") -def call_tree_structure(): - return CALL_TREE_STRUCTURE +def call_tree_data(): + return CALL_TREE_DATA diff --git a/tests/test_call_tree.py b/tests/test_call_tree.py index 3ed3bd9..df50c62 100644 --- a/tests/test_call_tree.py +++ b/tests/test_call_tree.py @@ -16,12 +16,12 @@ @pytest.fixture(scope="session") -def call_tree(call_tree_structure): - return CallTreeNode(**call_tree_structure) +def call_tree(call_tree_data): + return CallTreeNode(**call_tree_data) -def test_call_tree_validation_passes(call_tree_structure): - tree = CallTreeNode(**call_tree_structure) +def test_call_tree_validation_passes(call_tree_data): + tree = CallTreeNode(**call_tree_data) assert tree diff --git a/tests/test_trace_frame.py b/tests/test_trace_frame.py index 13b0ffb..cd22699 100644 --- a/tests/test_trace_frame.py +++ b/tests/test_trace_frame.py @@ -4,8 +4,8 @@ from evm_trace.base import TraceFrame -def test_trace_frame_validation_passes(trace_frame_structure): - frame = TraceFrame(**trace_frame_structure) +def test_trace_frame_validation_passes(trace_frame_data): + frame = TraceFrame(**trace_frame_data) assert frame @@ -17,7 +17,7 @@ def test_trace_frame_validation_passes(trace_frame_structure): {"storage": {"piggy": "dippin"}}, ), ) -def test_trace_frame_validation_fails(test_data, trace_frame_structure): - data = {**trace_frame_structure, **test_data} +def test_trace_frame_validation_fails(test_data, trace_frame_data): + data = {**trace_frame_data, **test_data} with pytest.raises(ValidationError): TraceFrame(**data) From b6510069badc545f17e14e08336deaa88a7ab79c Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 8 Jun 2022 16:24:42 -0500 Subject: [PATCH 3/4] test: add more tests --- evm_trace/base.py | 9 +++++--- tests/conftest.py | 46 +++++++++++++++++++++++++++++++++++++---- tests/test_call_tree.py | 20 +++++++++++++----- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/evm_trace/base.py b/evm_trace/base.py index 8ac956d..7a2fc43 100644 --- a/evm_trace/base.py +++ b/evm_trace/base.py @@ -171,10 +171,13 @@ def _create_node_from_call( # TODO: Handle internal nodes using JUMP and JUMPI - elif frame.op in ("SELFDESTRUCT", "STOP"): - # TODO: Handle the internal value transfer in SELFDESTRUCT + elif frame.op == "SELFDESTRUCT": + # TODO: Handle the internal value transfer + node.selfdestruct = True + break + + elif frame.op == "STOP": # TODO: Handle "execution halted" vs. gas limit reached - node.selfdestruct = frame.op == "SELFDESTRUCT" break elif frame.op in ("RETURN", "REVERT"): diff --git a/tests/conftest.py b/tests/conftest.py index 86f87d1..5c42e36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,7 @@ "aadb61a4b4c5d48b7a5669391b7c73852a3ab7795f24721b9a439220b54b591b": "0000000000000000000000000000000000000000000000000000000000000001", # noqa: E501 }, } -CALL_TREE_DATA = { +MUTABLE_CALL_TREE_DATA = { "call_type": CallType.MUTABLE, "address": HexBytes("0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488"), "value": 0, @@ -157,6 +157,41 @@ "failed": False, "displays_last": True, } +STATIC_CALL_TREE_DATA = { + "call_type": CallType.STATIC, + "address": HexBytes("0x274b028b03a250ca03644e6c578d81f019ee1323"), + "value": 0, + "depth": 2, + "gas_limit": 375554, + "gas_cost": 369688, + "calldata": HexBytes("0x7007cbe8"), + "returndata": HexBytes( + "0x000000000000000000000000000000000293b0e3558d33b8a4c483e40e2b8db9000000000000000000000000000000000000000000000000018b932eebcc7eb90000000000000000000000000000000000bf550935e92f79f09e3530df8660c5" # noqa: E501 + ), + "calls": [], + "selfdestruct": False, + "failed": False, +} +DELEGATE_CALL_TREE_DATA = { + "call_type": CallType.DELEGATE, + "address": HexBytes("0xaa1a02671440be41545d83bddff2bf2488628c10"), + "value": 0, + "depth": 3, + "gas_limit": 163575, + "gas_cost": 161021, + "calldata": HexBytes( + "0x70a0823100000000000000000000000077924185cf0cbb2ae0b746a0086a065d6875b0a5" + ), + "returndata": HexBytes("0x00000000000000000000000000000000000000000001136eac81315861fd80a7"), + "calls": [], + "selfdestruct": False, + "failed": False, +} +CALL_TREE_DATA_MAP = { + CallType.MUTABLE.value: MUTABLE_CALL_TREE_DATA, + CallType.STATIC.value: STATIC_CALL_TREE_DATA, + CallType.DELEGATE.value: DELEGATE_CALL_TREE_DATA, +} @pytest.fixture(scope="session") @@ -164,6 +199,9 @@ def trace_frame_data(): return TRACE_FRAME_DATA -@pytest.fixture(scope="session") -def call_tree_data(): - return CALL_TREE_DATA +@pytest.fixture( + scope="session", + params=[e.value for e in CallType if e not in (CallType.INTERNAL, CallType.SELFDESTRUCT)], +) +def call_tree_data(request): + yield CALL_TREE_DATA_MAP[request.param] diff --git a/tests/test_call_tree.py b/tests/test_call_tree.py index df50c62..263f425 100644 --- a/tests/test_call_tree.py +++ b/tests/test_call_tree.py @@ -1,8 +1,9 @@ import pytest from evm_trace import CallTreeNode +from evm_trace.base import CallType -EXPECTED_TREE_REPR = """ +MUTABLE_REPR = """ CALL: 0xF2Df0b975c0C9eFa2f8CA0491C2d1685104d2488 [194827 gas] ├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0x61d22ffe> [168423 gas] │ └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x8f27163e> [160842 gas] @@ -12,7 +13,16 @@ ├── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x8f27163e> [91277 gas] ├── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x90bb7141> [46476 gas] └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x90bb7141> [23491 gas] -""".strip() +""" +STATIC_REPR = "STATICCALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x7007cbe8> [369688 gas]" +DELEGATECALL_REPR = ( + "DELEGATECALL: 0xaa1A02671440Be41545D83BDDfF2bf2488628C10.<0x70a08231> [161021 gas]" +) +REPR_MAP = { + CallType.MUTABLE: MUTABLE_REPR, + CallType.STATIC: STATIC_REPR, + CallType.DELEGATE: DELEGATECALL_REPR, +} @pytest.fixture(scope="session") @@ -25,6 +35,6 @@ def test_call_tree_validation_passes(call_tree_data): assert tree -def test_call_tree_representation(call_tree): - actual = repr(call_tree) - assert actual == EXPECTED_TREE_REPR +def test_call_tree_mutable_representation(call_tree): + expected = REPR_MAP[call_tree.call_type].strip() + assert repr(call_tree) == expected From e90c716d912ca8d500623b58bc06f41bac670a7a Mon Sep 17 00:00:00 2001 From: unparalleled-js Date: Wed, 8 Jun 2022 16:30:34 -0500 Subject: [PATCH 4/4] fix: checksum issue --- evm_trace/display.py | 9 ++++++++- setup.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/evm_trace/display.py b/evm_trace/display.py index b6f80da..50d177d 100644 --- a/evm_trace/display.py +++ b/evm_trace/display.py @@ -32,7 +32,14 @@ def depth(self) -> int: def title(self) -> str: call_type = self.call.call_type.value.upper() call_mnemonic = "CALL" if self.call.call_type == CallType.MUTABLE else f"{call_type}CALL" - address = to_checksum_address(self.call.address.hex()) + address_hex_str = self.call.address.hex() + + try: + address = to_checksum_address(address_hex_str) + except ImportError: + # Ignore checksumming if user does not have eth-hash backend installed. + address = address_hex_str + cost = self.call.gas_cost call_path = str(address) diff --git a/setup.py b/setup.py index ec9c4d8..755352d 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ "pytest-xdist", # multi-process runner "pytest-cov", # Coverage analyzer plugin "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer + "eth-hash[pysha3]", # For eth-utils address checksumming ], "lint": [ "black>=22.3.0,<23.0", # auto-formatter and linter