Skip to content

Commit

Permalink
Merge pull request #14 from NotPeopling2day/feat/gas-report
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Sep 2, 2022
2 parents f38ce11 + 96d65f8 commit f4959be
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -e .[release]
- name: Build
run: python setup.py sdist bdist_wheel

Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: "3.8"
python-version: 3.8

- name: Install Dependencies
run: pip install .[lint]
run: |
python -m pip install --upgrade pip
pip install .[lint]
- name: Run Black
run: black --check .
Expand All @@ -35,10 +37,12 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: "3.8"
python-version: 3.8

- name: Install Dependencies
run: pip install .[lint,test] # Might need test deps
run: |
python -m pip install --upgrade pip
pip install .[lint,test]
- name: Run MyPy
run: mypy .
Expand All @@ -49,7 +53,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest] # eventually add `windows-latest`
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: [3.7, 3.8, 3.9, "3.10"]

steps:
- uses: actions/checkout@v2
Expand All @@ -60,7 +64,9 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Install Dependencies
run: pip install .[test]
run: |
python -m pip install --upgrade pip
pip install .[test]
- name: Run Tests
run: pytest -m "not fuzzing" -n 0 -s --cov
Expand All @@ -78,7 +84,7 @@ jobs:
# - name: Setup Python
# uses: actions/setup-python@v2
# with:
# python-version: "3.8"
# python-version: 3.8
#
# - name: Install Dependencies
# run: pip install .[test]
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repos:
- id: isort

- repo: https://github.com/psf/black
rev: 22.3.0
rev: 22.6.0
hooks:
- id: black
name: black
Expand All @@ -21,7 +21,7 @@ repos:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.960
rev: v0.971
hooks:
- id: mypy
additional_dependencies: [types-PyYAML, types-requests]
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ cd evm-trace
python3 -m venv venv
source venv/bin/activate

# install brownie into the virtual environment
# install evm-trace into the virtual environment
python setup.py install

# install the developer dependencies (-e is interactive mode)
pip install -e '.[dev]'
pip install -e .'[dev]'
```

## Pre-Commit Hooks
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,35 @@ class CustomDisplay(DisplayableCallTreeNode):
calltree = get_calltree_from_geth_trace(trace, display_cls=CustomDisplay)
```

### Gas Reports

If you are using a node that supports creating traces, you can get a gas report.

```python
from evm_trace.gas import get_gas_report

# see examples above for creating a calltree
calltree = get_calltree_from_geth_trace(trace, **root_node_kwargs)

gas_report = get_gas_report(calltree)
```

For a more custom report, use the `merge_reports` method to combine a list of reports into a single report.
Pass two or more `Dict[Any, Dict[Any, List[int]]]` to combine reports where `List[int]` is the gas used.

Customize the values of `Any` accordingly:

1. The first `Any` represents the bytes from the address.
2. The second `Any` represents the method selector.

For example, you may replace addresses with token names or selector bytes with signature call strings.

Import the method like so:

```python
from evm_trace.gas import merge_reports
```

## Development

This project is in development and should be considered a beta.
Expand Down
43 changes: 13 additions & 30 deletions evm_trace/base.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,34 @@
import math
from typing import Any, Dict, Iterator, List, Optional, Type
from typing import Dict, Iterator, List, Optional, Type

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

from evm_trace.display import DisplayableCallTreeNode
from evm_trace.enums import CallType


def _convert_hexbytes(cls, v: Any) -> HexBytes:
try:
return HexBytes(v)
except ValueError:
raise ValidationError(f"Value '{v}' could not be converted to Hexbytes.", cls)


class TraceFrame(BaseModel):
pc: int
op: str
gas: int
gas_cost: int = Field(alias="gasCost")
depth: int
stack: List[Any]
memory: List[Any]
storage: Dict[Any, Any] = {}

@validator("stack", "memory", pre=True, each_item=True)
def convert_hexbytes(cls, v) -> HexBytes:
return _convert_hexbytes(cls, v)

@validator("storage", pre=True)
def convert_hexbytes_dict(cls, v) -> Dict[HexBytes, HexBytes]:
return {_convert_hexbytes(cls, k): _convert_hexbytes(cls, val) for k, val in v.items()}
stack: List[HexBytes]
memory: List[HexBytes]
storage: Dict[HexBytes, HexBytes] = {}


class CallTreeNode(BaseModel):
call_type: CallType
address: Any
address: HexBytes = HexBytes("")
value: int = 0
depth: int = 0
gas_limit: Optional[int]
gas_cost: Optional[int] # calculated from call starting and return
calldata: Any = HexBytes(b"")
returndata: Any = HexBytes(b"")
calldata: HexBytes = HexBytes("")
returndata: HexBytes = HexBytes("")
calls: List["CallTreeNode"] = []
selfdestruct: bool = False
failed: bool = False
Expand All @@ -53,10 +38,6 @@ class CallTreeNode(BaseModel):
def display_nodes(self) -> Iterator[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])

Expand All @@ -68,7 +49,7 @@ def __getitem__(self, index: int) -> "CallTreeNode":


def get_calltree_from_geth_trace(
trace: Iterator[TraceFrame], show_internal=False, **root_node_kwargs
trace: Iterator[TraceFrame], show_internal: bool = False, **root_node_kwargs
) -> CallTreeNode:
"""
Creates a CallTreeNode from a given transaction trace.
Expand Down Expand Up @@ -166,7 +147,9 @@ def _create_node_from_call(
offset=frame.stack[-3], size=frame.stack[-4], memory=frame.memory
)

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

# TODO: Handle internal nodes using JUMP and JUMPI
Expand Down
2 changes: 1 addition & 1 deletion evm_trace/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def title(self) -> str:
address = to_checksum_address(address_hex_str) if address_hex_str else None
except ImportError:
# Ignore checksumming if user does not have eth-hash backend installed.
address = address_hex_str
address = address_hex_str # type: ignore

cost = self.call.gas_cost
call_path = str(address) if address else ""
Expand Down
52 changes: 52 additions & 0 deletions evm_trace/gas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import copy
from typing import Any, Dict, List

from evm_trace.base import CallTreeNode

GasReport = Dict[Any, Dict[Any, List[int]]]


def get_gas_report(calltree: CallTreeNode) -> GasReport:
"""
Extracts a gas report object from a :class:`~evm_trace.base.CallTreeNode`.
Args:
calltree (:class:`~evm_trace.base.CallTreeNode`): call tree used for gas report.
Returns:
:class:`~evm_trace.gas.Report`: Gas report structure from a call tree.
"""
report = {
calltree.address: {calltree.calldata[:4]: [calltree.gas_cost] if calltree.gas_cost else []}
}
return merge_reports(report, *map(get_gas_report, calltree.calls))


def merge_reports(*reports: GasReport) -> GasReport:
"""
Merge method for merging a list of gas reports and combining a list of gas costs.
"""
reports_ls = list(reports)
if len(reports_ls) < 1:
raise ValueError("Must be 2 or more reports to merge")
elif len(reports_ls) == 1:
return reports_ls[0]

merged_report: GasReport = copy.deepcopy(reports_ls.pop(0))

if len(reports_ls) < 1:
return merged_report

for report in reports_ls:
for outer_key, inner_dict in report.items():
if outer_key not in merged_report:
merged_report[outer_key] = inner_dict
continue

for inner_key, inner_list in report[outer_key].items():
if inner_key in merged_report[outer_key]:
merged_report[outer_key][inner_key].extend(inner_list)
else:
merged_report[outer_key][inner_key] = inner_list

return merged_report
19 changes: 10 additions & 9 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@

extras_require = {
"test": [ # `test` GitHub Action jobs uses this
"pytest>=6.0,<7.0", # Core testing package
"pytest>=6.0", # Core testing package
"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
"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
"black>=22.6.0", # auto-formatter and linter
"mypy>=0.971", # Static type analyzer
"flake8>=4.0.1", # Style linter
"isort>=5.10.1", # Import sorting linter
],
"release": [ # `release` GitHub Action job uses this
"setuptools", # Installation tool
Expand Down Expand Up @@ -55,10 +55,11 @@
include_package_data=True,
install_requires=[
"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
"pydantic>=1.10.1,<2.0",
"hexbytes>=0.3.0,<1.0.0",
"eth-utils>=2.0.0",
"ethpm-types>=0.3.7,<0.4.0",
],
python_requires=">=3.7.2,<4",
extras_require=extras_require,
py_modules=["evm_trace"],
Expand Down
37 changes: 37 additions & 0 deletions tests/test_gas_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import List

from ethpm_types import HexBytes

from evm_trace import CallTreeNode
from evm_trace.gas import GasReport, get_gas_report, merge_reports

# Simplified version of gas reports only for testing purposes
reports: List[GasReport] = [
{
HexBytes("1"): {HexBytes("10"): [1]},
HexBytes("2"): {HexBytes("20"): [2]},
},
{
HexBytes("1"): {HexBytes("10"): [1]},
HexBytes("2"): {HexBytes("21"): [2]},
HexBytes("3"): {HexBytes("30"): [3]},
},
]


def test_builds_gas_report(call_tree_data):
tree = CallTreeNode(**call_tree_data)
gas_report = get_gas_report(tree)

for call in tree.calls:
assert call.address in gas_report


def test_merged_reports():
merged = merge_reports(*reports)

assert merged == {
HexBytes("0x01"): {HexBytes("0x10"): [1, 1]},
HexBytes("0x02"): {HexBytes("0x20"): [2], HexBytes("0x21"): [2]},
HexBytes("0x03"): {HexBytes("0x30"): [3]},
}

0 comments on commit f4959be

Please sign in to comment.