diff --git a/evm_trace/__init__.py b/evm_trace/__init__.py index e598e7d..9013df0 100644 --- a/evm_trace/__init__.py +++ b/evm_trace/__init__.py @@ -1,10 +1,13 @@ -from .base import CallTreeNode, CallType, TraceFrame, get_calltree_from_geth_trace +from .base import CallTreeNode +from .enums import CallType +from .geth import TraceFrame, get_calltree_from_geth_call_trace, get_calltree_from_geth_trace from .parity import ParityTrace, ParityTraceList, get_calltree_from_parity_trace __all__ = [ "CallTreeNode", "CallType", "get_calltree_from_geth_trace", + "get_calltree_from_geth_call_trace", "get_calltree_from_parity_trace", "ParityTrace", "ParityTraceList", diff --git a/evm_trace/base.py b/evm_trace/base.py index dddc150..377875c 100644 --- a/evm_trace/base.py +++ b/evm_trace/base.py @@ -1,44 +1,60 @@ -import math -from typing import Dict, Iterator, List, Optional +from typing import List, Optional -from eth_utils import to_int from ethpm_types import BaseModel, HexBytes -from pydantic import Field, validator +from pydantic import validator from evm_trace.display import get_tree_display from evm_trace.enums import CallType -class TraceFrame(BaseModel): - pc: int - op: str - gas: int - gas_cost: int = Field(alias="gasCost") - depth: int - stack: List[HexBytes] = [] - memory: List[HexBytes] = [] - storage: Dict[HexBytes, HexBytes] = {} - - @validator("pc", "gas", "gas_cost", "depth", pre=True) - def validate_ints(cls, value): - return int(value, 16) if isinstance(value, str) else value - - class CallTreeNode(BaseModel): + """ + A higher-level object modeling a node in an execution call tree. + Used by both Geth-style and Parity-style low-level data structures. + """ + call_type: CallType + """The type of call.""" + address: HexBytes = HexBytes("") + """The contract address of the call.""" + value: int = 0 + """The amount of value sent on the call.""" + depth: int = 0 + """ + The number of external jumps away the initially called contract (starts at 0). + """ + gas_limit: Optional[int] = None + """ + The total amount of gas available. + """ + gas_cost: Optional[int] = None # calculated from call starting and return + """The cost to execute this opcode.""" + calldata: HexBytes = HexBytes("") + """Transaction calldata (inputs).""" + returndata: HexBytes = HexBytes("") + """Transaction returndata (outputs).""" + calls: List["CallTreeNode"] = [] + """The list of external sub-calls this call makes.""" + selfdestruct: bool = False + """Whether this is a SELFDESTRUCT opcode or not.""" + failed: bool = False + """Whether the call failed or not.""" def __str__(self) -> str: - return get_tree_display(self) + try: + return get_tree_display(self) + except Exception as err: + return f"CallTreeNode (display_err={err})" def __repr__(self) -> str: return str(self) @@ -46,133 +62,17 @@ def __repr__(self) -> str: def __getitem__(self, index: int) -> "CallTreeNode": return self.calls[index] + @validator("calldata", "returndata", "address", pre=True) + def validate_bytes(cls, value): + return HexBytes(value) if isinstance(value, str) else value -def get_calltree_from_geth_trace( - trace: Iterator[TraceFrame], show_internal: bool = 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. - Defaults to ``False``. - root_node_kwargs (dict): Keyword arguments 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("") - - 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) - - end_index = stop_word + 1 - byte_slice = b"".join(memory[start_word:end_index]) - offset_index = offset_int % 32 - - # NOTE: Add 4 for the selector. - - end_bytes_index = offset_index + size_int - return_bytes = byte_slice[offset_index:end_bytes_index] - return HexBytes(return_bytes) - + @validator("value", "depth", pre=True) + def validate_ints(cls, value): + if not value: + return 0 -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/ - """ + return int(value, 16) if isinstance(value, str) else value - if show_internal: - raise NotImplementedError() - - node = CallTreeNode(**node_kwargs) - for frame in trace: - if frame.op in ("CALL", "DELEGATECALL", "STATICCALL"): - child_node_kwargs = { - "address": frame.stack[-2][-20:], - "depth": frame.depth, - "gas_limit": int(frame.stack[-1].hex(), 16), - "gas_cost": frame.gas_cost, - } - - # TODO: Validate gas values - - if frame.op == "CALL": - child_node_kwargs["call_type"] = CallType.CALL - 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.DELEGATECALL - child_node_kwargs["calldata"] = _extract_memory( - offset=frame.stack[-3], size=frame.stack[-4], memory=frame.memory - ) - else: - child_node_kwargs["call_type"] = CallType.STATICCALL - 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, show_internal=show_internal, **child_node_kwargs - ) - node.calls.append(child_node) - - # TODO: Handle internal nodes using JUMP and JUMPI - - 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 - break - - elif frame.op in ("RETURN", "REVERT") and not node.returndata: - 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 + @validator("gas_limit", "gas_cost", pre=True) + def validate_optional_ints(cls, value): + return int(value, 16) if isinstance(value, str) else value diff --git a/evm_trace/geth.py b/evm_trace/geth.py new file mode 100644 index 0000000..2212a5d --- /dev/null +++ b/evm_trace/geth.py @@ -0,0 +1,230 @@ +import math +from typing import Dict, Iterator, List + +from eth_utils import to_int +from ethpm_types import BaseModel, HexBytes +from pydantic import Field, validator + +from evm_trace.base import CallTreeNode +from evm_trace.enums import CallType + + +class TraceFrame(BaseModel): + """ + A low-level data structure modeling a transaction trace frame + from the Geth RPC ``debug_traceTransaction``. + """ + + pc: int + """Program counter.""" + + op: str + """Opcode.""" + + gas: int + """Remaining gas.""" + + gas_cost: int = Field(alias="gasCost") + """The cost to execute this opcode.""" + + depth: int + """ + The number of external jumps away the initially called contract (starts at 0). + """ + + stack: List[HexBytes] = [] + """Execution stack.""" + + memory: List[HexBytes] = [] + """Execution memory.""" + + storage: Dict[HexBytes, HexBytes] = {} + """Contract storage.""" + + @validator("pc", "gas", "gas_cost", "depth", pre=True) + def validate_ints(cls, value): + return int(value, 16) if isinstance(value, str) else value + + +def get_calltree_from_geth_call_trace(data: Dict) -> CallTreeNode: + """ + Creates a CallTreeNode from a given transaction call trace. + + Args: + data (Dict): The response from ``debug_traceTransaction`` when using + ``tracer=callTracer``. + + Returns: + :class:`~evm_trace.base.CallTreeNode`: Call tree of transaction trace. + """ + + data = _validate_data_from_call_tracer(data) + root = CallTreeNode(**data) + + def fix_depth(r: CallTreeNode): + for c in r.calls: + c.depth = r.depth + 1 + fix_depth(c) + + fix_depth(root) + return root + + +def get_calltree_from_geth_trace( + trace: Iterator[TraceFrame], show_internal: bool = 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. + Defaults to ``False``. + root_node_kwargs (dict): Keyword arguments 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("") + + 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) + + end_index = stop_word + 1 + byte_slice = b"".join(memory[start_word:end_index]) + offset_index = offset_int % 32 + + # NOTE: Add 4 for the selector. + + 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( + 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"): + + # NOTE: Because of the different meanings in structLog style gas values, + # gas is not set for nodes created this way. + child_node_kwargs = {"address": frame.stack[-2][-20:], "depth": frame.depth} + + if frame.op == "CALL": + child_node_kwargs["call_type"] = CallType.CALL + 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.DELEGATECALL + child_node_kwargs["calldata"] = _extract_memory( + offset=frame.stack[-3], size=frame.stack[-4], memory=frame.memory + ) + else: + child_node_kwargs["call_type"] = CallType.STATICCALL + 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, show_internal=show_internal, **child_node_kwargs + ) + node.calls.append(child_node) + + # TODO: Handle internal nodes using JUMP and JUMPI + + 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 + break + + elif frame.op in ("RETURN", "REVERT") and not node.returndata: + 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 + + +def _validate_data_from_call_tracer(data: Dict) -> Dict: + # Handle renames + if "receiver" in data: + data["address"] = data.pop("receiver") + elif "to" in data: + data["address"] = data.pop("to") + if "input" in data: + data["calldata"] = data.pop("input") + if "output" in data: + data["returndata"] = data.pop("output") + if "gasUsed" in data: + data["gas_cost"] = data.pop("gasUsed") + if "gas" in data: + data["gas_limit"] = data.pop("gas") + if "type" in data: + data["call_type"] = data.pop("type") + + # Remove unneeded keys + unneeded_keys = ("sender", "from") + for key in unneeded_keys: + if key in data: + del data[key] + + # Handle sub calls + def fix_call_calls(r): + r["calls"] = [ + _validate_data_from_call_tracer(x) for x in r.get("calls", []) if isinstance(x, dict) + ] + + fix_call_calls(data) + return data diff --git a/evm_trace/parity.py b/evm_trace/parity.py index a4f0ba3..667dabe 100644 --- a/evm_trace/parity.py +++ b/evm_trace/parity.py @@ -2,11 +2,16 @@ from pydantic import BaseModel, Field, validator -from evm_trace.base import CallTreeNode, CallType +from evm_trace.base import CallTreeNode +from evm_trace.enums import CallType class CallAction(BaseModel): gas: int + """ + The amount of gas available for the action. + """ + input: Optional[str] = None receiver: Optional[str] = Field(alias="to", default=None) sender: str = Field(alias="from") @@ -21,6 +26,10 @@ def convert_integer(cls, v): class CreateAction(BaseModel): gas: int + """ + The amount of gas available for the action. + """ + init: str value: int @@ -35,22 +44,40 @@ class SelfDestructAction(BaseModel): @validator("balance", pre=True) def convert_integer(cls, v): - return int(v, 16) + return int(v, 16) if isinstance(v, str) else int(v) class ActionResult(BaseModel): - gas_used: str = Field(alias="gasUsed") + """ + A base class for various OP-code-specified actions + in Parity ``trace_transaction`` output. + """ + + gas_used: int = Field(alias="gasUsed") + """ + The amount of gas utilized by the action. It does *not* + include the ``21,000`` base fee or the data costs of ``4`` + for gas per zero byte and ``16`` gas per non-zero byte. + """ @validator("gas_used", pre=True) def convert_integer(cls, v): - return int(v, 16) + return int(v, 16) if isinstance(v, str) else int(v) class CallResult(ActionResult): + """ + The result of CALL. + """ + output: str class CreateResult(ActionResult): + """ + The result of CREATE. + """ + address: str code: str @@ -161,7 +188,7 @@ def get_calltree_from_parity_trace( address=selfdestruct_action.address, ) - trace_list: List[ParityTrace] = [x for x in traces] + trace_list: List[ParityTrace] = list(traces) subtraces: List[ParityTrace] = [ sub for sub in trace_list diff --git a/tests/conftest.py b/tests/conftest.py index a735ee5..ecbe599 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ import pytest from hexbytes import HexBytes -from evm_trace.base import CallType +from evm_trace.enums import CallType TRACE_FRAME_DATA = { "pc": 1564, @@ -187,6 +187,200 @@ "selfdestruct": False, "failed": False, } +GETH_CALL_TRACE_DATA = { + "type": "CALL", + "from": "0x1e59ce931b4cfea3fe4b875411e280e173cb7a9c", + "to": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "value": "0x7b", + "gas": "0x47cb6e", + "gasUsed": "0x445e6", + "input": "0x372dca07", + "output": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", # noqa: E501 + "calls": [ + { + "type": "CALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "value": "0x0", + "gas": "0x4697e7", + "gasUsed": "0x84d4", + "input": "0x045856de00000000000000000000000000000000000000000000000000000000000393cc", + "output": "0x00000000000000000000000000000000000000000000000000001564ff3f0da300000000000000000000000000000000000000000000000000000002964619c700000000000000000000000000000000000000000000000000000000000393cc00000000000000000000000000000000000000000000000004cae9c39bdb4f7700000000000000000000000000000000000000000000000000005af310694bb20000000000000000000000000000000000000000011dc18b6f8f1601b7b1b33100000000000000000000000000000000000000000000000000000000000393cc000000000000000000000000000000000000000000000004ffd72d92184e6bb20000000000000000000000000000000000000000000000000000000000000d7e000000000000000000000000000000000000000000000006067396b875234f7700000000000000000000000000000000000000000000000000012f39bc807bb20000000000000000000000000000000000000002f5db749b3db467538fb1b331", # noqa: E501 + }, + { + "type": "CALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "value": "0x0", + "gas": "0x45fb34", + "gasUsed": "0xf3eb", + "input": "0xbeed0f8500000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000011dc18b6f8f1601b7b1b33100000000000000000000000000000000000000000000000000000000000000096963652d637265616d0000000000000000000000000000000000000000000000", # noqa: E501 + "output": "0x", + "calls": [ + { + "type": "STATICCALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "gas": "0x44b63f", + "gasUsed": "0x5d9", + "input": "0x7007cbe8", + "output": "0x000000000000000000000000000000000293b0e3558d33b8a4c483e40e2b8db9000000000000000000000000000000000000000000000000018b932eebcc7eb90000000000000000000000000000000000bf550935e92f79f09e3530df8660c5", # noqa: E501 + }, + { + "type": "CALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "value": "0x0", + "gas": "0x44a772", + "gasUsed": "0xa1f2", + "input": "0x878fb70100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000011dc18b6f8f1601b7b1b331000000000000000000000000f2df0b975c0c9efa2f8ca0491c2d1685104d2488000000000000000000000000000000000000000000000000000000000000000773696d706c657200000000000000000000000000000000000000000000000000", # noqa: E501 + "output": "0x", + }, + ], + }, + { + "type": "CALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "value": "0x0", + "gas": "0x4506ef", + "gasUsed": "0x2d3", + "input": "0xb27b88040000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", + "output": "0x0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", + }, + { + "type": "CALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "value": "0x0", + "gas": "0x450126", + "gasUsed": "0x165ab", + "input": "0xb9e5b20a0000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", + "output": "0x", + "calls": [ + { + "type": "STATICCALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "gas": "0x43e944", + "gasUsed": "0x947", + "input": "0xe5e1d93f000000000000000000000000f2df0b975c0c9efa2f8ca0491c2d1685104d2488", # noqa: E501 + "output": "0x00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000011dc18b6f8f1601b7b1b331000000000000000000000000f2df0b975c0c9efa2f8ca0491c2d1685104d2488000000000000000000000000000000000000000000000000000000000000000773696d706c657200000000000000000000000000000000000000000000000000", # noqa: E501 + }, + { + "type": "CALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "value": "0x0", + "gas": "0x43d5bd", + "gasUsed": "0x8442", + "input": "0x878fb70100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000274b028b03a250ca03644e6c578d81f019ee1323000000000000000000000000000000000000000000000000000000000000000773696d706c657200000000000000000000000000000000000000000000000000", # noqa: E501 + "output": "0x", + }, + { + "type": "CALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "value": "0x0", + "gas": "0x4348e6", + "gasUsed": "0x6253", + "input": "0x90bb7141", + "output": "0x", + }, + { + "type": "CALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "value": "0x0", + "gas": "0x42e652", + "gasUsed": "0x5a83", + "input": "0x90bb7141", + "output": "0x", + }, + ], + }, + { + "type": "STATICCALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "gas": "0x439750", + "gasUsed": "0xafe", + "input": "0xbff2e0950000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + { + "type": "STATICCALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "gas": "0x437fe8", + "gasUsed": "0x35b", + "input": "0x9155fd570000000000000000000000001e59ce931b4cfea3fe4b875411e280e173cb7a9c", + "output": "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + { + "type": "CALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "value": "0x0", + "gas": "0x43784b", + "gasUsed": "0x7f83", + "input": "0xbeed0f850000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000096c656d6f6e64726f700000000000000000000000000000000000000000000000", # noqa: E501 + "output": "0x", + "calls": [ + { + "type": "STATICCALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "gas": "0x426124", + "gasUsed": "0x5d9", + "input": "0x7007cbe8", + "output": "0x000000000000000000000000000000000293b0e3558d33b8a4c483e40e2b8db9000000000000000000000000000000000000000000000000018b932eebcc7eb90000000000000000000000000000000000bf550935e92f79f09e3530df8660c5", # noqa: E501 + }, + { + "type": "CALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "value": "0x0", + "gas": "0x425257", + "gasUsed": "0x649e", + "input": "0x878fb70100000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f2df0b975c0c9efa2f8ca0491c2d1685104d2488000000000000000000000000000000000000000000000000000000000000000773696d706c657200000000000000000000000000000000000000000000000000", # noqa: E501 + "output": "0x", + }, + ], + }, + { + "type": "CALL", + "from": "0xf2df0b975c0c9efa2f8ca0491c2d1685104d2488", + "to": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "value": "0x0", + "gas": "0x42f728", + "gasUsed": "0x7f83", + "input": "0xbeed0f850000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006f0000000000000000000000000000000000000000000000000000000000000014736e6974636865735f6765745f73746963686573000000000000000000000000", # noqa: E501 + "output": "0x", + "calls": [ + { + "type": "STATICCALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "gas": "0x41e206", + "gasUsed": "0x5d9", + "input": "0x7007cbe8", + "output": "0x000000000000000000000000000000000293b0e3558d33b8a4c483e40e2b8db9000000000000000000000000000000000000000000000000018b932eebcc7eb90000000000000000000000000000000000bf550935e92f79f09e3530df8660c5", # noqa: E501 + }, + { + "type": "CALL", + "from": "0xbcf7fffd8b256ec51a36782a52d0c34f6474d951", + "to": "0x274b028b03a250ca03644e6c578d81f019ee1323", + "value": "0x0", + "gas": "0x41d339", + "gasUsed": "0x649e", + "input": "0x878fb7010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000006f000000000000000000000000f2df0b975c0c9efa2f8ca0491c2d1685104d2488000000000000000000000000000000000000000000000000000000000000000773696d706c657200000000000000000000000000000000000000000000000000", # noqa: E501 + "output": "0x", + }, + ], + }, + ], +} CALL_TREE_DATA_MAP = { CallType.CALL.value: MUTABLE_CALL_TREE_DATA, CallType.STATICCALL.value: STATIC_CALL_TREE_DATA, @@ -199,6 +393,11 @@ def trace_frame_data(): return TRACE_FRAME_DATA +@pytest.fixture(scope="session") +def geth_call_trace_data(): + return GETH_CALL_TRACE_DATA + + @pytest.fixture( scope="session", params=(CallType.CALL.value, CallType.DELEGATECALL.value, CallType.STATICCALL.value), diff --git a/tests/test_call_tree.py b/tests/test_base.py similarity index 53% rename from tests/test_call_tree.py rename to tests/test_base.py index ba6d827..f16cdb0 100644 --- a/tests/test_call_tree.py +++ b/tests/test_base.py @@ -1,7 +1,7 @@ import pytest -from evm_trace import CallTreeNode -from evm_trace.base import CallType +from evm_trace.base import CallTreeNode +from evm_trace.enums import CallType from .expected_traces import ( CALL_TRACE_EXPECTED_OUTPUT, @@ -21,11 +21,11 @@ def call_tree(call_tree_data): return CallTreeNode(**call_tree_data) -def test_call_tree_validation_passes(call_tree_data): - tree = CallTreeNode(**call_tree_data) - assert tree +class TestCallTreeNode: + def test_call_tree_validation_passes(self, call_tree_data): + tree = CallTreeNode(**call_tree_data) + assert tree - -def test_call_tree_mutable_representation(call_tree): - expected = EXPECTED_OUTPUT_MAP[call_tree.call_type].strip() - assert repr(call_tree) == expected + def test_call_tree_mutable_representation(self, call_tree): + expected = EXPECTED_OUTPUT_MAP[call_tree.call_type].strip() + assert repr(call_tree) == expected diff --git a/tests/test_gas_report.py b/tests/test_gas.py similarity index 100% rename from tests/test_gas_report.py rename to tests/test_gas.py diff --git a/tests/test_geth.py b/tests/test_geth.py new file mode 100644 index 0000000..d415b6c --- /dev/null +++ b/tests/test_geth.py @@ -0,0 +1,90 @@ +import pytest +from hexbytes import HexBytes +from pydantic import ValidationError + +from evm_trace.enums import CallType +from evm_trace.geth import ( + TraceFrame, + get_calltree_from_geth_call_trace, + get_calltree_from_geth_trace, +) + + +class TestTraceFrame: + def test_validation_passes(self, trace_frame_data): + frame = TraceFrame(**trace_frame_data) + assert frame + + def test_no_memory(self): + pc = 0 + op = "REVERT" + gas = 4732305 + gas_cost = 3 + depth = 1 + stack = [] + trace_frame = TraceFrame(pc=pc, op=op, gas=gas, gasCost=gas_cost, depth=depth, stack=stack) + assert trace_frame.pc == pc + assert trace_frame.op == op + assert trace_frame.gas == gas + assert trace_frame.gas_cost == gas_cost + assert trace_frame.depth == depth + assert trace_frame.stack == [] + + @pytest.mark.parametrize( + "test_data", + ( + {"stack": ["potato"]}, + {"memory": ["potato"]}, + {"storage": {"piggy": "dippin"}}, + ), + ) + def test_validation_fails(self, test_data, trace_frame_data): + data = {**trace_frame_data, **test_data} + with pytest.raises(ValidationError): + TraceFrame(**data) + + +def test_get_calltree_from_geth_trace(trace_frame_data): + trace_frame_data["op"] = "RETURN" + returndata = HexBytes("0x0000000000000000000000004d4d2c55eae97a04acafb66011df29463b665732") + root_node_kwargs = { + "gas_cost": 123, + "gas_limit": 1234, + "address": "0x56764a0000000000000000000000000000000031", + "calldata": HexBytes("0x21325"), + "value": 34, + "call_type": CallType.CALL, + "failed": False, + "returndata": returndata, + } + frames = (TraceFrame(**d) for d in (trace_frame_data,)) + actual = get_calltree_from_geth_trace(frames, **root_node_kwargs) + + # Tests against a bug where we could not set the return data. + assert actual.returndata == returndata + + +def test_get_calltree_from_geth_call_trace(geth_call_trace_data): + node = get_calltree_from_geth_call_trace(geth_call_trace_data) + expected = """ + CALL: 0xF2Df0b975c0C9eFa2f8CA0491C2d1685104d2488.<0x372dca07> [280038 gas] +├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0x045856de> [34004 gas] +├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0xbeed0f85> [62443 gas] +│ ├── STATICCALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x7007cbe8> [1497 gas] +│ └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x878fb701> [41458 gas] +├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0xb27b8804> [723 gas] +├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0xb9e5b20a> [91563 gas] +│ ├── STATICCALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0xe5e1d93f> [2375 gas] +│ ├── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x878fb701> [33858 gas] +│ ├── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x90bb7141> [25171 gas] +│ └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x90bb7141> [23171 gas] +├── STATICCALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0xbff2e095> [2814 gas] +├── STATICCALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0x9155fd57> [859 gas] +├── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0xbeed0f85> [32643 gas] +│ ├── STATICCALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x7007cbe8> [1497 gas] +│ └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x878fb701> [25758 gas] +└── CALL: 0xBcF7FFFD8B256Ec51a36782a52D0c34f6474D951.<0xbeed0f85> [32643 gas] + ├── STATICCALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x7007cbe8> [1497 gas] + └── CALL: 0x274b028b03A250cA03644E6c578D81f019eE1323.<0x878fb701> [25758 gas] + """ + assert repr(node) == expected.strip() diff --git a/tests/test_parity_trace.py b/tests/test_parity.py similarity index 100% rename from tests/test_parity_trace.py rename to tests/test_parity.py diff --git a/tests/test_trace_frame.py b/tests/test_trace_frame.py deleted file mode 100644 index 3df4884..0000000 --- a/tests/test_trace_frame.py +++ /dev/null @@ -1,61 +0,0 @@ -import pytest -from hexbytes import HexBytes -from pydantic import ValidationError - -from evm_trace import CallType -from evm_trace.base import TraceFrame, get_calltree_from_geth_trace - - -def test_trace_frame_validation_passes(trace_frame_data): - frame = TraceFrame(**trace_frame_data) - assert frame - - -def test_trace_no_memory(): - pc = 0 - op = "REVERT" - gas = 4732305 - gas_cost = 3 - depth = 1 - stack = [] - trace_frame = TraceFrame(pc=pc, op=op, gas=gas, gasCost=gas_cost, depth=depth, stack=stack) - assert trace_frame.pc == pc - assert trace_frame.op == op - assert trace_frame.gas == gas - assert trace_frame.gas_cost == gas_cost - assert trace_frame.depth == depth - assert trace_frame.stack == [] - - -@pytest.mark.parametrize( - "test_data", - ( - {"stack": ["potato"]}, - {"memory": ["potato"]}, - {"storage": {"piggy": "dippin"}}, - ), -) -def test_trace_frame_validation_fails(test_data, trace_frame_data): - data = {**trace_frame_data, **test_data} - with pytest.raises(ValidationError): - TraceFrame(**data) - - -def test_get_calltree_from_geth_trace(trace_frame_data): - trace_frame_data["op"] = "RETURN" - returndata = HexBytes("0x0000000000000000000000004d4d2c55eae97a04acafb66011df29463b665732") - root_node_kwargs = { - "gas_cost": 123, - "gas_limit": 1234, - "address": "0x56764a0000000000000000000000000000000031", - "calldata": HexBytes("0x21325"), - "value": 34, - "call_type": CallType.CALL, - "failed": False, - "returndata": returndata, - } - frames = (TraceFrame(**d) for d in (trace_frame_data,)) - actual = get_calltree_from_geth_trace(frames, **root_node_kwargs) - - # Tests against a bug where we could not set the return data. - assert actual.returndata == returndata