Skip to content

Commit 72a068a

Browse files
authored
Merge pull request #10 from banteg/feat/vmtrace
feat: add vmTrace support
2 parents 64f4031 + 3166d75 commit 72a068a

File tree

2 files changed

+206
-4
lines changed

2 files changed

+206
-4
lines changed

.github/workflows/test.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Setup Python
1313
uses: actions/setup-python@v2
1414
with:
15-
python-version: 3.8
15+
python-version: "3.8"
1616

1717
- name: Install Dependencies
1818
run: pip install .[lint]
@@ -35,7 +35,7 @@ jobs:
3535
- name: Setup Python
3636
uses: actions/setup-python@v2
3737
with:
38-
python-version: 3.8
38+
python-version: "3.8"
3939

4040
- name: Install Dependencies
4141
run: pip install .[lint,test] # Might need test deps
@@ -49,7 +49,7 @@ jobs:
4949
strategy:
5050
matrix:
5151
os: [ubuntu-latest, macos-latest] # eventually add `windows-latest`
52-
python-version: [3.7, 3.8, 3.9]
52+
python-version: ["3.7", "3.8", "3.9", "3.10"]
5353

5454
steps:
5555
- uses: actions/checkout@v2
@@ -78,7 +78,7 @@ jobs:
7878
# - name: Setup Python
7979
# uses: actions/setup-python@v2
8080
# with:
81-
# python-version: 3.8
81+
# python-version: "3.8"
8282
#
8383
# - name: Install Dependencies
8484
# run: pip install .[test]

evm_trace/vmtrace.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict, Iterator, List, Optional, Type, Union
4+
5+
from eth.vm.memory import Memory # type: ignore
6+
from eth.vm.stack import Stack # type: ignore
7+
from eth_utils import to_checksum_address
8+
from hexbytes import HexBytes
9+
10+
try:
11+
from msgspec import Struct # type: ignore
12+
from msgspec.json import Decoder # type: ignore
13+
except ImportError as e:
14+
raise ImportError("msgspec not found. install it with `pip install msgspec`") from e
15+
16+
# opcodes grouped by the number of items they pop from the stack
17+
# fmt: off
18+
POP_OPCODES = {
19+
1: ["EXTCODEHASH", "ISZERO", "NOT", "BALANCE", "CALLDATALOAD", "EXTCODESIZE", "BLOCKHASH", "POP", "MLOAD", "SLOAD", "JUMP", "SELFDESTRUCT"], # noqa: E501
20+
2: ["SHL", "SHR", "SAR", "REVERT", "ADD", "MUL", "SUB", "DIV", "SDIV", "MOD", "SMOD", "EXP", "SIGNEXTEND", "LT", "GT", "SLT", "SGT", "EQ", "AND", "XOR", "OR", "BYTE", "SHA3", "MSTORE", "MSTORE8", "SSTORE", "JUMPI", "RETURN"], # noqa: E501
21+
3: ["RETURNDATACOPY", "ADDMOD", "MULMOD", "CALLDATACOPY", "CODECOPY", "CREATE"],
22+
4: ["CREATE2", "EXTCODECOPY"],
23+
6: ["STATICCALL", "DELEGATECALL"],
24+
7: ["CALL", "CALLCODE"]
25+
}
26+
# fmt: on
27+
POPCODES = {op: n for n, opcodes in POP_OPCODES.items() for op in opcodes}
28+
POPCODES.update({f"LOG{n}": n + 2 for n in range(0, 5)})
29+
POPCODES.update({f"SWAP{i}": i + 1 for i in range(1, 17)})
30+
POPCODES.update({f"DUP{i}": i for i in range(1, 17)})
31+
32+
33+
class uint256(int):
34+
pass
35+
36+
37+
class VMTrace(Struct):
38+
code: HexBytes
39+
"""The code to be executed."""
40+
ops: List[VMOperation]
41+
"""The operations executed."""
42+
43+
44+
class VMOperation(Struct):
45+
pc: int
46+
"""The program counter."""
47+
cost: int
48+
"""The gas cost for this instruction."""
49+
ex: Optional[VMExecutedOperation]
50+
"""Information concerning the execution of the operation."""
51+
sub: Optional[VMTrace]
52+
"""Subordinate trace of the CALL/CREATE if applicable."""
53+
op: str
54+
"""Opcode that is being called."""
55+
idx: str
56+
"""Index in the tree."""
57+
58+
59+
class VMExecutedOperation(Struct):
60+
used: int
61+
"""The amount of remaining gas."""
62+
push: List[HexBytes]
63+
"""The stack item placed, if any."""
64+
mem: Optional[MemoryDiff]
65+
"""If altered, the memory delta."""
66+
store: Optional[StorageDiff]
67+
"""The altered storage value, if any."""
68+
69+
70+
class MemoryDiff(Struct):
71+
off: int
72+
"""Offset into memory the change begins."""
73+
data: HexBytes
74+
"""The changed data."""
75+
76+
77+
class StorageDiff(Struct):
78+
key: uint256
79+
"""Which key in storage is changed."""
80+
val: uint256
81+
"""What the value has been changed to."""
82+
83+
84+
class VMTraceFrame(Struct):
85+
"""
86+
A synthetic trace frame represening the state at a step of execution.
87+
"""
88+
89+
address: str
90+
pc: int
91+
op: str
92+
depth: int
93+
stack: List[int]
94+
memory: Union[bytes, memoryview]
95+
storage: Dict[int, int]
96+
97+
98+
def to_address(value):
99+
# clear the padding and expand to 32 bytes
100+
return to_checksum_address(value[-20:].rjust(20, b"\x00"))
101+
102+
103+
def to_trace_frames(
104+
trace: VMTrace,
105+
depth: int = 1,
106+
address: str = None,
107+
copy_memory: bool = True,
108+
) -> Iterator[VMTraceFrame]:
109+
"""
110+
Replays a VMTrace and yields trace frames at each step of the execution.
111+
Can be used as a much faster drop-in replacement for Geth-style traces.
112+
113+
Arguments:
114+
trace (VMTrace):
115+
a decoded trace from a `trace_` rpc.
116+
depth (int):
117+
a depth of the call being processed. automatically populated.
118+
address (str):
119+
the address of the contract being executed. auto populated except the root call.
120+
copy_memory (bool):
121+
whether to copy memory when returning trace frames. disable for a speedup when dealing
122+
with traces using a large amount of memory. when disabled, `VMTraceFrame.memory` becomes
123+
`memoryview` instead of `bytes`, which works like a pointer at the memory `bytearray`.
124+
this means you must process the frames immediately, otherwise you risk memory value
125+
mutating further into execution.
126+
127+
Returns:
128+
Iterator[VMTraceFrame]:
129+
an iterator of synthetic traces which can be used as a drop-in replacement for
130+
Geth-style traces. also contains the address of the current contract context.
131+
"""
132+
memory = Memory()
133+
stack = Stack()
134+
storage: Dict[int, int] = {}
135+
call_address = None
136+
read_memory = memory.read_bytes if copy_memory else memory.read
137+
138+
for op in trace.ops:
139+
if op.ex and op.ex.mem:
140+
memory.extend(op.ex.mem.off, len(op.ex.mem.data))
141+
142+
# geth convention is to return after memory expansion, but before the operation is applied
143+
yield VMTraceFrame(
144+
address=address,
145+
pc=op.pc,
146+
op=op.op,
147+
depth=depth,
148+
stack=[val for typ, val in stack.values],
149+
memory=read_memory(0, len(memory)),
150+
storage=storage.copy(),
151+
)
152+
153+
if op.op in ["CALL", "DELEGATECALL", "STATICCALL"]:
154+
call_address = to_address(stack.values[-2][1])
155+
156+
if op.ex:
157+
if op.ex.mem:
158+
memory.write(op.ex.mem.off, len(op.ex.mem.data), op.ex.mem.data)
159+
160+
num_pop = POPCODES.get(op.op)
161+
if num_pop:
162+
stack.pop_any(num_pop)
163+
164+
for item in op.ex.push:
165+
stack.push_bytes(item)
166+
167+
if op.ex.store:
168+
storage[op.ex.store.key] = op.ex.store.val
169+
170+
if op.sub:
171+
yield from to_trace_frames(
172+
op.sub, depth=depth + 1, address=call_address, copy_memory=copy_memory
173+
)
174+
175+
176+
class RPCResponse(Struct):
177+
result: Union[RPCTraceResult, List[RPCTraceResult]]
178+
179+
180+
class RPCTraceResult(Struct):
181+
trace: Optional[List]
182+
vmTrace: VMTrace
183+
stateDiff: Optional[Dict]
184+
185+
186+
def dec_hook(type: Type, obj: Any) -> Any:
187+
if type is uint256:
188+
return uint256(obj, 16)
189+
if type is HexBytes:
190+
return HexBytes(obj)
191+
192+
193+
def from_rpc_response(buffer: bytes) -> Union[VMTrace, List[VMTrace]]:
194+
"""
195+
Decode structured data from a raw `trace_replayTransaction` or `trace_replayBlockTransactions`.
196+
"""
197+
resp = Decoder(RPCResponse, dec_hook=dec_hook).decode(buffer)
198+
199+
if isinstance(resp.result, list):
200+
return [i.vmTrace for i in resp.result]
201+
else:
202+
return resp.result.vmTrace

0 commit comments

Comments
 (0)