|
| 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