Skip to content

Commit

Permalink
feat: add calltree creation method (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
NotPeopling2day committed May 9, 2022
1 parent f344d29 commit cd08dbe
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 10 deletions.
5 changes: 0 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ repos:
hooks:
- id: check-yaml

- repo: https://github.com/asottile/seed-isort-config
rev: v2.2.0
hooks:
- id: seed-isort-config

- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.9.3
hooks:
Expand Down
4 changes: 2 additions & 2 deletions evm_trace/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .base import CallTreeNode, CallType, TraceFrame
from .base import CallTreeNode, CallType, TraceFrame, get_calltree_from_trace

__all__ = ["CallTreeNode", "CallType", "TraceFrame"]
__all__ = ["CallTreeNode", "CallType", "get_calltree_from_trace", "TraceFrame"]
127 changes: 126 additions & 1 deletion evm_trace/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import math
from enum import Enum
from typing import Any, Dict, List
from typing import Any, Dict, Iterator, List

from eth_utils import to_int
from hexbytes import HexBytes
from pydantic import BaseModel, Field, ValidationError, validator

Expand Down Expand Up @@ -43,6 +45,7 @@ class CallTreeNode(BaseModel):
call_type: CallType
address: Any
value: int = 0
depth: int = 0
gas_limit: int
gas_cost: int # calculated from call starting and return
calldata: Any = HexBytes(b"")
Expand All @@ -54,3 +57,125 @@ class CallTreeNode(BaseModel):
@validator("address", "calldata", "returndata", pre=True)
def validate_hexbytes(cls, v) -> HexBytes:
return _convert_hexbytes(cls, v)


def get_calltree_from_trace(
trace: Iterator[TraceFrame], show_internal=False, **root_node_kwargs
) -> CallTreeNode:
"""
Creates a CallTreeNode from a given transaction 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``.
Returns:
:class:`~evm_trace.base.CallTreeNode: Call tree of transaction trace.
"""

return _create_node_from_call(
trace=trace,
show_internal=show_internal,
**root_node_kwargs,
)


def _extract_memory(offset: HexBytes, size: HexBytes, memory: List[HexBytes]) -> HexBytes:
"""
Extracts memory from the EVM stack.
Args:
offset (HexBytes): Offset byte location in memory.
size (HexBytes): Number of bytes to return.
memory (List[HexBytes]): Memory stack.
Returns:
HexBytes: Byte value from memory stack.
"""

size_int = to_int(size)

if size_int == 0:
return HexBytes(b"")

offset_int = to_int(offset)

# Compute the word that contains the first byte
start_word = math.floor(offset_int / 32)
# Compute the word that contains the last byte
stop_word = math.ceil((offset_int + size_int) / 32)

byte_slice = b""

for word in memory[start_word:stop_word]:
byte_slice += word

offset_index = offset_int % 32
return HexBytes(byte_slice[offset_index:size_int])


def _create_node_from_call(
trace: Iterator[TraceFrame], show_internal: bool = False, **node_kwargs
) -> CallTreeNode:
"""
Use specified opcodes to create a branching callnode
https://www.evm.codes/
"""

if show_internal:
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:] # 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
child_node_kwargs["value"] = int(frame.stack[-3].hex(), 16)
child_node_kwargs["calldata"] = _extract_memory(
offset=frame.stack[-4], size=frame.stack[-5], memory=frame.memory
)
elif frame.op == "DELEGATECALL":
child_node_kwargs["call_type"] = CallType.DELEGATE
child_node_kwargs["calldata"] = _extract_memory(
offset=frame.stack[-3], size=frame.stack[-4], memory=frame.memory
)
else:
child_node_kwargs["call_type"] = CallType.STATIC
child_node_kwargs["calldata"] = _extract_memory(
offset=frame.stack[-3], size=frame.stack[-4], memory=frame.memory
)

child_node = _create_node_from_call(trace=trace, **child_node_kwargs)
node.calls.append(child_node)

# TODO: Handle internal nodes using JUMP and JUMPI

elif frame.op in ("SELFDESTRUCT", "STOP"):
# TODO: Handle the internal value transfer in SELFDESTRUCT
# TODO: Handle "execution halted" vs. gas limit reached
node.selfdestruct = frame.op == "SELFDESTRUCT"
break

elif frame.op in ("RETURN", "REVERT"):
node.returndata = _extract_memory(
offset=frame.stack[-1], size=frame.stack[-2], memory=frame.memory
)
# TODO: Handle "execution halted" vs. gas limit reached
node.failed = frame.op == "REVERT"
break

# TODO: Handle invalid opcodes (`node.failed = True`)
# NOTE: ignore other opcodes

# TODO: Handle "execution halted" vs. gas limit reached
return node
2 changes: 0 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,5 @@ markers = "fuzzing: Run Hypothesis fuzz test suite"
line_length = 100
force_grid_wrap = 0
include_trailing_comma = true
known_third_party = ["hexbytes", "pydantic", "pytest", "setuptools"]
known_first_party = ["MODULE_NAME"]
multi_line_output = 3
use_parentheses = true
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +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",
], # NOTE: Add 3rd party libraries here
python_requires=">=3.7.2,<4",
extras_require=extras_require,
Expand Down

0 comments on commit cd08dbe

Please sign in to comment.